From 93b627b149502542502987e226f78ffec71da59b Mon Sep 17 00:00:00 2001 From: Gijs van Dam Date: Thu, 19 Jun 2025 13:41:27 +0200 Subject: [PATCH 1/9] sphinx: remove dead code and tiny refactor This commit removes an unused var and changes bytes.Compare to the idiomatic bytes.Equal. --- sphinx_test.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/sphinx_test.go b/sphinx_test.go index 362af3c..8c84651 100644 --- a/sphinx_test.go +++ b/sphinx_test.go @@ -106,8 +106,7 @@ func newTestRoute(numHops int) ([]*Router, *PaymentPath, *[]HopData, *OnionPacke func TestBolt4Packet(t *testing.T) { var ( - route PaymentPath - hopsData []HopData + route PaymentPath ) for i, pubKeyHex := range bolt4PubKeys { pubKeyBytes, err := hex.DecodeString(pubKeyHex) @@ -125,7 +124,6 @@ func TestBolt4Packet(t *testing.T) { OutgoingCltv: uint32(i), } copy(hopData.NextAddress[:], bytes.Repeat([]byte{byte(i)}, 8)) - hopsData = append(hopsData, hopData) hopPayload, err := NewLegacyHopPayload(&hopData) if err != nil { @@ -157,7 +155,7 @@ func TestBolt4Packet(t *testing.T) { t.Fatalf("unable to decode onion packet: %v", err) } - if bytes.Compare(b.Bytes(), finalPacket) != 0 { + if !bytes.Equal(b.Bytes(), finalPacket) { t.Fatalf("final packet does not match expected BOLT 4 packet, "+ "want: %s, got %s", hex.EncodeToString(finalPacket), hex.EncodeToString(b.Bytes())) From 75af2edf849299e1289311db803965dc8e2dee22 Mon Sep 17 00:00:00 2001 From: Gijs van Dam Date: Mon, 23 Jun 2025 15:19:14 +0200 Subject: [PATCH 2/9] 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. --- path_test.go | 245 +++++++++++++++++- .../blinded-onion-message-onion-test.json | 143 ++++++++++ 2 files changed, 385 insertions(+), 3 deletions(-) create mode 100644 testdata/blinded-onion-message-onion-test.json diff --git a/path_test.go b/path_test.go index 9a301d9..909b6bb 100644 --- a/path_test.go +++ b/path_test.go @@ -11,9 +11,19 @@ import ( "github.com/stretchr/testify/require" ) +//nolint:lll const ( - routeBlindingTestFileName = "testdata/route-blinding-test.json" - onionRouteBlindingTestFileName = "testdata/onion-route-blinding-test.json" + routeBlindingTestFileName = "testdata/route-blinding-test.json" + onionRouteBlindingTestFileName = "testdata/onion-route-blinding-test.json" + blindedOnionMessageOnionTestFileName = "testdata/blinded-onion-message-onion-test.json" +) + +var ( + // bolt4PubKeys contains the public keys used in the Bolt 4 spec. test + // vectors. We convert them to variables named after the commonly used + // names in cryptography. + alicePubKey = bolt4PubKeys[0] + bobPubKey = bolt4PubKeys[1] ) // TestBuildBlindedRoute tests BuildBlindedRoute and decryptBlindedHopData against @@ -117,6 +127,181 @@ func TestBuildBlindedRoute(t *testing.T) { } } +// TestBuildOnionMessageBlindedRoute tests the construction of a blinded route +// for an onion message, specifically the concatenation of two blinded paths, +// against the spec. test vectors in `blinded-onion-message-onion-test.json`. It +// verifies the correctness of BuildBlindedPath, decryptBlindedHopData, and +// NextEphemeral. +// +// The test setup involves several parties and two distinct blinded paths that +// are combined to form the full route: +// +// 1. Path from Dave: Dave (the receiver) first constructs a blinded path for a +// message to be sent from Bob to himself (Dave). +// The path is: Bob -> Carol -> Dave +// +// 2. Path from Sender: Dave gives his blinded path to a Sender. The Sender +// then creates their own blinded path from themselves to Bob, passing +// through Alice. The path is: Sender -> Alice -> Bob +// +// 3. Path Concatenation: The Sender prepends their path to Dave's path, +// creating a final, concatenated route: +// Sender -> Alice -> Bob -> Carol -> Dave +// To link the two paths, the Sender includes a `next_path_key_override` +// in the payload for Alice. This override is set to the first path key +// (blinding point) of Dave's path, instructing Alice to use it for the next +// hop (Bob) instead of the key that she could derive herself. +// +// The test then asserts that the generated concatenated path matches the test +// vector's expected route. It simulates the decryption process at each hop, +// verifying that at each hop encrypted_recipient_data is what we expect it to +// be and that it correctly decrypts to the encrypted_data_tlv stream. Finally, +// we verify the derivation of the next ephemeral key. +func TestBuildOnionMessageBlindedRoute(t *testing.T) { + t.Parallel() + + // First, we'll read out the raw Json file at the target location. + jsonBytes, err := os.ReadFile(blindedOnionMessageOnionTestFileName) + require.NoError(t, err) + + // Once we have the raw file, we'll unpack it into our + // onionMessageJsonTestCase struct defined below. + testCase := &onionMessageJsonTestCase{} + require.NoError(t, json.Unmarshal(jsonBytes, testCase)) + require.Len(t, testCase.Generate.Hops, 4) + + // buildMessagePath is a helper closure used to convert + // hopOnionMessageData objects into HopInfo objects. + buildMessagePath := func(h []hopOnionMessageData, + initialHopID string) []*HopInfo { + + path := make([]*HopInfo, len(h)) + + // The json test vector doesn't properly specify the current + // node id, so we need the initial Node ID as a starting point. + currentHop := initialHopID + for i, hop := range h { + nodeIDStr, err := hex.DecodeString(currentHop) + require.NoError(t, err) + nodeID, err := btcec.ParsePubKey(nodeIDStr) + require.NoError(t, err) + payload, err := hex.DecodeString(hop.EncryptedDataTlv) + require.NoError(t, err) + + path[i] = &HopInfo{ + NodePub: nodeID, + PlainText: payload, + } + + // The json test vector doesn't properly specify the + // current node id. It does specify the next node id. So + // to get the current node id for the next iteration, we + // get the next node id here. + currentHop = hop.EncodedOnionMessageTLVs.NextNodeID + } + + return path + } + + // First, Dave will build a blinded path from Bob to itself. + receiverSessKey := privKeyFromString( + testCase.Generate.Hops[1].PathKeySecret, + ) + daveBobPath := buildMessagePath( + testCase.Generate.Hops[1:], bobPubKey, + ) + daveBobBlindedPath, err := BuildBlindedPath( + receiverSessKey, daveBobPath, + ) + require.NoError(t, err) + + // At this point, Dave will give his blinded path to the Sender who will + // then build its own blinded route from itself to Bob via Alice. The + // sender will then concatenate the two paths. Note that in the payload + // for Alice, the `next_path_key_override` field is added which is set + // to the first path key in Dave's blinded route. This will indicate to + // Alice that she should use this point for the next path key instead of + // the next path key that she derives. + // Path created by Dave: Bob -> Carol -> Dave + // Path that the Sender will build: Sender -> Alice -> Bob + aliceBobPath := buildMessagePath( + testCase.Generate.Hops[:1], alicePubKey, + ) + senderSessKey := privKeyFromString( + testCase.Generate.Hops[0].PathKeySecret, + ) + aliceBobBlindedPath, err := BuildBlindedPath( + senderSessKey, aliceBobPath, + ) + require.NoError(t, err) + + // Construct the concatenated path. + path := &BlindedPath{ + IntroductionPoint: aliceBobBlindedPath.Path.IntroductionPoint, + BlindingPoint: aliceBobBlindedPath.Path.BlindingPoint, + BlindedHops: append( + aliceBobBlindedPath.Path.BlindedHops, + daveBobBlindedPath.Path.BlindedHops..., + ), + } + + // Check that the constructed path is equal to the test vector path. + require.True(t, equalPubKeys( + testCase.Route.FirstNodeId, path.IntroductionPoint, + )) + require.True(t, equalPubKeys( + testCase.Route.FirstPathKey, path.BlindingPoint, + )) + + for i, hop := range testCase.Route.Hops { + require.True(t, equalPubKeys( + hop.BlindedNodeID, path.BlindedHops[i].BlindedNodePub, + )) + + data, _ := hex.DecodeString(hop.EncryptedRecipientData) + require.Equal(t, data, path.BlindedHops[i].CipherText) + } + + // Assert that each hop is able to decode the encrypted data meant for + // it. + for i, hop := range testCase.Decrypt.Hops { + genData := testCase.Generate.Hops[i] + priv := privKeyFromString(hop.PrivKey) + ephem := pubKeyFromString(genData.EphemeralPubKey) + + // Check if the encrypted_recipient_data is what we expect it to + // be. + encRecipientDataExpected, err := hex.DecodeString( + genData.EncryptedRecipientData, + ) + require.NoError(t, err) + require.Equal( + t, encRecipientDataExpected, + path.BlindedHops[i].CipherText, + ) + + // Now we'll decrypt the blinded hop data using the private key + // and the ephemeral public key. + data, err := decryptBlindedHopData( + &PrivKeyECDH{PrivKey: priv}, ephem, + path.BlindedHops[i].CipherText, + ) + require.NoError(t, err) + + // Check if the decrypted data is what we expect it to be. + dataExpected, err := hex.DecodeString(genData.EncryptedDataTlv) + require.NoError(t, err) + require.Equal(t, dataExpected, data) + + nextEphem, err := NextEphemeral(&PrivKeyECDH{priv}, ephem) + require.NoError(t, err) + + nextE := privKeyFromString(genData.NextEphemeralPrivKey) + + require.Equal(t, nextE.PubKey(), nextEphem) + } +} + // TestOnionRouteBlinding tests that an onion packet can correctly be processed // by a node in a blinded route. func TestOnionRouteBlinding(t *testing.T) { @@ -223,24 +408,48 @@ type decryptData struct { Hops []decryptHops `json:"hops"` } +type decryptOnionMessageData struct { + Hops []decryptOnionMessageHops `json:"hops"` +} + type decryptHops struct { + //nolint:tagliatelle Onion string `json:"onion"` NodePrivKey string `json:"node_privkey"` NextBlinding string `json:"next_blinding"` } +type decryptOnionMessageHops struct { + OnionMessage string `json:"onion_message"` + PrivKey string `json:"privkey"` + NextNodeID string `json:"next_node_id"` +} + type blindingJsonTestCase struct { Generate generateData `json:"generate"` Route routeData `json:"route"` Unblind unblindData `json:"unblind"` } +type onionMessageJsonTestCase struct { + Generate generateOnionMessageData `json:"generate"` + Route routeOnionMessageData `json:"route"` + // OnionMessage onionMessageData `json:"onionmessage"` + Decrypt decryptOnionMessageData `json:"decrypt"` +} + type routeData struct { IntroductionNodeID string `json:"introduction_node_id"` Blinding string `json:"blinding"` Hops []blindedHop `json:"hops"` } +type routeOnionMessageData struct { + FirstNodeId string `json:"first_node_id"` + FirstPathKey string `json:"first_path_key"` + Hops []blindedOnionMessageHop `json:"hops"` +} + type unblindData struct { Hops []unblindedHop `json:"hops"` } @@ -249,8 +458,14 @@ type generateData struct { Hops []hopData `json:"hops"` } +type generateOnionMessageData struct { + SessionKey string `json:"session_key"` + Hops []hopOnionMessageData `json:"hops"` +} + type unblindedHop struct { - NodePrivKey string `json:"node_privkey"` + NodePrivKey string `json:"node_privkey"` + //nolint:tagliatelle EphemeralPubKey string `json:"ephemeral_pubkey"` DecryptedData string `json:"decrypted_data"` NextEphemeralPubKey string `json:"next_ephemeral_pubkey"` @@ -262,11 +477,35 @@ type hopData struct { EncodedTLVs string `json:"encoded_tlvs"` } +type hopOnionMessageData struct { + PathKeySecret string `json:"path_key_secret"` + EncodedOnionMessageTLVs encodedOnionMessageTLVs `json:"tlvs"` + EncryptedDataTlv string `json:"encrypted_data_tlv"` //nolint:lll + EphemeralPubKey string `json:"E"` //nolint:tagliatelle + NextEphemeralPrivKey string `json:"next_e"` + EncryptedRecipientData string `json:"encrypted_recipient_data"` //nolint:lll +} + +type encodedOnionMessageTLVs struct { + NextNodeID string `json:"next_node_id"` + NextPathKeyOverride string `json:"next_path_key_override"` + PathKeyOverrideSecret string `json:"path_key_override_secret"` + PathID string `json:"path_id"` + // The test vector provides more fields, but since we don't want to pull + // in the tlv package, they are omitted here. They should be tested in + // higher layer tests. +} + type blindedHop struct { BlindedNodeID string `json:"blinded_node_id"` EncryptedData string `json:"encrypted_data"` } +type blindedOnionMessageHop struct { + BlindedNodeID string `json:"blinded_node_id"` + EncryptedRecipientData string `json:"encrypted_recipient_data"` +} + func equalPubKeys(pkStr string, pk *btcec.PublicKey) bool { return hex.EncodeToString(pk.SerializeCompressed()) == pkStr } diff --git a/testdata/blinded-onion-message-onion-test.json b/testdata/blinded-onion-message-onion-test.json new file mode 100644 index 0000000..fe5191e --- /dev/null +++ b/testdata/blinded-onion-message-onion-test.json @@ -0,0 +1,143 @@ +{ + "comment": "Test vector creating an onionmessage, including joining an existing one", + "generate": { + "comment": "This sections contains test data for Dave's blinded path Bob->Dave; sender has to prepend a hop to Alice to reach Bob", + "session_key": "0303030303030303030303030303030303030303030303030303030303030303", + "hops": [ + { + "alias": "Alice", + "comment": "Alice->Bob: note next_path_key_override to match that give by Dave for Bob", + "path_key_secret": "6363636363636363636363636363636363636363636363636363636363636363", + "tlvs": { + "next_node_id": "0324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c", + "next_path_key_override": "031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f", + "path_key_override_secret": "0101010101010101010101010101010101010101010101010101010101010101" + }, + "encrypted_data_tlv": "04210324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c0821031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f", + "ss": "c04d2a4c518241cb49f2800eea92554cb543f268b4c73f85693541e86d649205", + "HMAC256('blinded_node_id', ss)": "bc5388417c8db33af18ab7ba43f6a5641861f7b0ecb380e501a739af446a7bf4", + "blinded_node_id": "02d1c3d73f8cac67e7c5b6ec517282d5ba0a52b06a29ec92ff01e12decf76003c1", + "E": "031195a8046dcbb8e17034bca630065e7a0982e4e36f6f7e5a8d4554e4846fcd99", + "H(E || ss)": "83377bd6096f82df3a46afec20d68f3f506168f2007f6e86c2dc267417de9e34", + "next_e": "bf3e8999518c0bb6e876abb0ae01d44b9ba211720048099a2ba5a83afd730cad01", + "rho": "6926df9d4522b26ad4330a51e3481208e4816edd9ae4feaf311ea0342eb90c44", + "encrypted_recipient_data": "49531cf38d3280b7f4af6d6461a2b32e3df50acfd35176fc61422a1096eed4dfc3806f29bf74320f712a61c766e7f7caac0c42f86040125fbaeec0c7613202b206dbdd31fda56394367b66a711bfd7d5bedbe20bed1b" + }, + { + "alias": "Bob", + "comment": "Bob->Carol", + "path_key_secret": "0101010101010101010101010101010101010101010101010101010101010101", + "tlvs": { + "next_node_id": "027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007", + "unknown_tag_561": "123456" + }, + "encrypted_data_tlv": "0421027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007fd023103123456", + "ss": "196f1f3e0be9d65f88463c1ab63e07f41b4e7c0368c28c3e6aa290cc0d22eaed", + "HMAC256('blinded_node_id', ss)": "c331d35827bdd509a02f1e64d48c7f0d7b2603355abbb1a3733c86e50135608e", + "blinded_node_id": "03f1465ca5cf3ec83f16f9343d02e6c24b76993a93e1dea2398f3147a9be893d7a", + "E": "031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f", + "H(E || ss)": "1889a6cf337d9b34f80bb23a91a2ca194e80d7614f0728bdbda153da85e46b69", + "next_e": "f7ab6dca6152f7b6b0c9d7c82d716af063d72d8eef8816dfc51a8ae828fa7dce01", + "rho": "db991242ce366ab44272f38383476669b713513818397a00d4808d41ea979827", + "encrypted_recipient_data": "adf6771d3983b7f543d1b3d7a12b440b2bd3e1b3b8d6ec1023f6dec4f0e7548a6f57f6dbe9573b0a0f24f7c5773a7dd7a7bdb6bd0ee686d759f5" + }, + { + "alias": "Carol", + "comment": "Carol->Dave", + "path_key_secret": "f7ab6dca6152f7b6b0c9d7c82d716af063d72d8eef8816dfc51a8ae828fa7dce", + "tlvs": { + "padding": "0000000000", + "next_node_id": "032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991" + }, + "encrypted_data_tlv": "010500000000000421032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991", + "ss": "c7b33d74a723e26331a91c15ae5bc77db28a18b801b6bc5cd5bba98418303a9d", + "HMAC256('blinded_node_id', ss)": "a684c7495444a8cc2a6dfdecdf0819f3cdf4e86b81cc14e39825a40872ecefff", + "blinded_node_id": "035dbc0493aa4e7eea369d6a06e8013fd03e66a5eea91c455ed65950c4942b624b", + "E": "02b684babfd400c8dd48b367e9754b8021a3594a34dc94d7101776c7f6a86d0582", + "H(E || ss)": "2d80c5619a5a68d22dd3d784cab584c2718874922735d36cb36a179c10a796ca", + "next_e": "5de52bb427cc148bf23e509fdc18012004202517e80abcfde21612ae408e6cea01", + "rho": "739851e89b61cab34ee9ba7d5f3c342e4adc8b91a72991664026f68a685f0bdc", + "encrypted_recipient_data": "d8903df7a79ac799a0b59f4ba22f6a599fa32e7ff1a8325fc22b88d278ce3e4840af02adfb82d6145a189ba50c2219c9e4351e634d198e0849ac" + }, + { + "alias": "Dave", + "comment": "Dave is final node, hence path_id", + "path_key_secret": "5de52bb427cc148bf23e509fdc18012004202517e80abcfde21612ae408e6cea", + "tlvs": { + "padding": "", + "path_id": "deadbeefbadc0ffeedeadbeefbadc0ffeedeadbeefbadc0ffeedeadbeefbadc0", + "unknown_tag_65535": "06c1" + }, + "encrypted_data_tlv": "01000620deadbeefbadc0ffeedeadbeefbadc0ffeedeadbeefbadc0ffeedeadbeefbadc0fdffff0206c1", + "ss": "024955ed0d4ebbfab13498f5d7aacd00bf096c8d9ed0473cdfc96d90053c86b7", + "HMAC256('blinded_node_id', ss)": "3f5612df60f050ac571aeaaf76655e138529bea6d23293ebe15659f2588cd039", + "blinded_node_id": "0237bf019fa0fbecde8b4a1c7b197c9c1c76f9a23d67dd55bb5e42e1f50bb771a6", + "E": "025aaca62db7ce6b46386206ef9930daa32e979a35cb185a41cb951aa7d254b03c", + "H(E || ss)": "db5719e79919d706eab17eebaad64bd691e56476a42f0e26ae60caa9082f56fa", + "next_e": "ae31d2fbbf2f59038542c13287b9b624ea1a212c82be87c137c3d92aa30a185d01", + "rho": "c47cde57edc790df7b9b6bf921aff5e5eee43f738ab8fa9103ef675495f3f50e", + "encrypted_recipient_data": "bdc03f088764c6224c8f939e321bf096f363b2092db381fc8787f891c8e6dc9284991b98d2a63d9f91fe563065366dd406cd8e112cdaaa80d0e6" + } + ] + }, + "route": { + "comment": "The resulting blinded route Alice to Dave.", + "first_node_id": "02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619", + "first_path_key": "031195a8046dcbb8e17034bca630065e7a0982e4e36f6f7e5a8d4554e4846fcd99", + "hops": [ + { + "blinded_node_id": "02d1c3d73f8cac67e7c5b6ec517282d5ba0a52b06a29ec92ff01e12decf76003c1", + "encrypted_recipient_data": "49531cf38d3280b7f4af6d6461a2b32e3df50acfd35176fc61422a1096eed4dfc3806f29bf74320f712a61c766e7f7caac0c42f86040125fbaeec0c7613202b206dbdd31fda56394367b66a711bfd7d5bedbe20bed1b" + }, + { + "blinded_node_id": "03f1465ca5cf3ec83f16f9343d02e6c24b76993a93e1dea2398f3147a9be893d7a", + "encrypted_recipient_data": "adf6771d3983b7f543d1b3d7a12b440b2bd3e1b3b8d6ec1023f6dec4f0e7548a6f57f6dbe9573b0a0f24f7c5773a7dd7a7bdb6bd0ee686d759f5" + }, + { + "blinded_node_id": "035dbc0493aa4e7eea369d6a06e8013fd03e66a5eea91c455ed65950c4942b624b", + "encrypted_recipient_data": "d8903df7a79ac799a0b59f4ba22f6a599fa32e7ff1a8325fc22b88d278ce3e4840af02adfb82d6145a189ba50c2219c9e4351e634d198e0849ac" + }, + { + "blinded_node_id": "0237bf019fa0fbecde8b4a1c7b197c9c1c76f9a23d67dd55bb5e42e1f50bb771a6", + "encrypted_recipient_data": "bdc03f088764c6224c8f939e321bf096f363b2092db381fc8787f891c8e6dc9284991b98d2a63d9f91fe563065366dd406cd8e112cdaaa80d0e6" + } + ] + }, + "onionmessage": { + "comment": "An onion message which sends a 'hello' to Dave", + "unknown_tag_1": "68656c6c6f", + "onion_message_packet": "0002531fe6068134503d2723133227c867ac8fa6c83c537e9a44c3c5bdbdcb1fe33793b828776d70aabbd8cef1a5b52d5a397ae1a20f20435ff6057cd8be339d5aee226660ef73b64afa45dbf2e6e8e26eb96a259b2db5aeecda1ce2e768bbc35d389d7f320ca3d2bd14e2689bef2f5ac0307eaaabc1924eb972c1563d4646ae131accd39da766257ed35ea36e4222527d1db4fa7b2000aab9eafcceed45e28b5560312d4e2299bd8d1e7fe27d10925966c28d497aec400b4630485e82efbabc00550996bdad5d6a9a8c75952f126d14ad2cff91e16198691a7ef2937de83209285f1fb90944b4e46bca7c856a9ce3da10cdf2a7d00dc2bf4f114bc4d3ed67b91cbde558ce9af86dc81fbdc37f8e301b29e23c1466659c62bdbf8cff5d4c20f0fb0851ec72f5e9385dd40fdd2e3ed67ca4517117825665e50a3e26f73c66998daf18e418e8aef9ce2d20da33c3629db2933640e03e7b44c2edf49e9b482db7b475cfd4c617ae1d46d5c24d697846f9f08561eac2b065f9b382501f6eabf07343ed6c602f61eab99cdb52adf63fd44a8db2d3016387ea708fc1c08591e19b4d9984ebe31edbd684c2ea86526dd8c7732b1d8d9117511dc1b643976d356258fce8313b1cb92682f41ab72dedd766f06de375f9edacbcd0ca8c99b865ea2b7952318ea1fd20775a28028b5cf59dece5de14f615b8df254eee63493a5111ea987224bea006d8f1b60d565eef06ac0da194dba2a6d02e79b2f2f34e9ca6e1984a507319d86e9d4fcaeea41b4b9144e0b1826304d4cc1da61cfc5f8b9850697df8adc5e9d6f3acb3219b02764b4909f2b2b22e799fd66c383414a84a7d791b899d4aa663770009eb122f90282c8cb9cda16aba6897edcf9b32951d0080c0f52be3ca011fbec3fb16423deb47744645c3b05fdbd932edf54ba6efd26e65340a8e9b1d1216582e1b30d64524f8ca2d6c5ba63a38f7120a3ed71bed8960bcac2feee2dd41c90be48e3c11ec518eb3d872779e4765a6cc28c6b0fa71ab57ced73ae963cc630edae4258cba2bf25821a6ae049fec2fca28b5dd1bb004d92924b65701b06dcf37f0ccd147a13a03f9bc0f98b7d78fe9058089756931e2cd0e0ed92ec6759d07b248069526c67e9e6ce095118fd3501ba0f858ef030b76c6f6beb11a09317b5ad25343f4b31aef02bc555951bc7791c2c289ecf94d5544dcd6ad3021ed8e8e3db34b2a73e1eedb57b578b068a5401836d6e382110b73690a94328c404af25e85a8d6b808893d1b71af6a31fadd8a8cc6e31ecc0d9ff7e6b91fd03c274a5c1f1ccd25b61150220a3fddb04c91012f5f7a83a5c90deb2470089d6e38cd5914b9c946eca6e9d31bbf8667d36cf87effc3f3ff283c21dd4137bd569fe7cf758feac94053e4baf7338bb592c8b7c291667fadf4a9bf9a2a154a18f612cbc7f851b3f8f2070e0a9d180622ee4f8e81b0ab250d504cef24116a3ff188cc829fcd8610b56343569e8dc997629410d1967ca9dd1d27eec5e01e4375aad16c46faba268524b154850d0d6fe3a76af2c6aa3e97647c51036049ac565370028d6a439a2672b6face56e1b171496c0722cfa22d9da631be359661617c5d5a2d286c5e19db9452c1e21a0107b6400debda2decb0c838f342dd017cdb2dccdf1fe97e3df3f881856b546997a3fed9e279c720145101567dd56be21688fed66bf9759e432a9aa89cbbd225d13cdea4ca05f7a45cfb6a682a3d5b1e18f7e6cf934fae5098108bae9058d05c3387a01d8d02a656d2bfff67e9f46b2d8a6aac28129e52efddf6e552214c3f8a45bc7a912cca9a7fec1d7d06412c6972cb9e3dc518983f56530b8bffe7f92c4b6eb47d4aef59fb513c4653a42de61bc17ad7728e7fc7590ff05a9e991de03f023d0aaf8688ed6170def5091c66576a424ac1cb" + }, + "decrypt": { + "comment": "This section contains the internal values generated by intermediate nodes when decrypting the onion.", + "hops": [ + { + "alias": "Alice", + "privkey": "4141414141414141414141414141414141414141414141414141414141414141", + "onion_message": "0201031195a8046dcbb8e17034bca630065e7a0982e4e36f6f7e5a8d4554e4846fcd9905560002531fe6068134503d2723133227c867ac8fa6c83c537e9a44c3c5bdbdcb1fe33793b828776d70aabbd8cef1a5b52d5a397ae1a20f20435ff6057cd8be339d5aee226660ef73b64afa45dbf2e6e8e26eb96a259b2db5aeecda1ce2e768bbc35d389d7f320ca3d2bd14e2689bef2f5ac0307eaaabc1924eb972c1563d4646ae131accd39da766257ed35ea36e4222527d1db4fa7b2000aab9eafcceed45e28b5560312d4e2299bd8d1e7fe27d10925966c28d497aec400b4630485e82efbabc00550996bdad5d6a9a8c75952f126d14ad2cff91e16198691a7ef2937de83209285f1fb90944b4e46bca7c856a9ce3da10cdf2a7d00dc2bf4f114bc4d3ed67b91cbde558ce9af86dc81fbdc37f8e301b29e23c1466659c62bdbf8cff5d4c20f0fb0851ec72f5e9385dd40fdd2e3ed67ca4517117825665e50a3e26f73c66998daf18e418e8aef9ce2d20da33c3629db2933640e03e7b44c2edf49e9b482db7b475cfd4c617ae1d46d5c24d697846f9f08561eac2b065f9b382501f6eabf07343ed6c602f61eab99cdb52adf63fd44a8db2d3016387ea708fc1c08591e19b4d9984ebe31edbd684c2ea86526dd8c7732b1d8d9117511dc1b643976d356258fce8313b1cb92682f41ab72dedd766f06de375f9edacbcd0ca8c99b865ea2b7952318ea1fd20775a28028b5cf59dece5de14f615b8df254eee63493a5111ea987224bea006d8f1b60d565eef06ac0da194dba2a6d02e79b2f2f34e9ca6e1984a507319d86e9d4fcaeea41b4b9144e0b1826304d4cc1da61cfc5f8b9850697df8adc5e9d6f3acb3219b02764b4909f2b2b22e799fd66c383414a84a7d791b899d4aa663770009eb122f90282c8cb9cda16aba6897edcf9b32951d0080c0f52be3ca011fbec3fb16423deb47744645c3b05fdbd932edf54ba6efd26e65340a8e9b1d1216582e1b30d64524f8ca2d6c5ba63a38f7120a3ed71bed8960bcac2feee2dd41c90be48e3c11ec518eb3d872779e4765a6cc28c6b0fa71ab57ced73ae963cc630edae4258cba2bf25821a6ae049fec2fca28b5dd1bb004d92924b65701b06dcf37f0ccd147a13a03f9bc0f98b7d78fe9058089756931e2cd0e0ed92ec6759d07b248069526c67e9e6ce095118fd3501ba0f858ef030b76c6f6beb11a09317b5ad25343f4b31aef02bc555951bc7791c2c289ecf94d5544dcd6ad3021ed8e8e3db34b2a73e1eedb57b578b068a5401836d6e382110b73690a94328c404af25e85a8d6b808893d1b71af6a31fadd8a8cc6e31ecc0d9ff7e6b91fd03c274a5c1f1ccd25b61150220a3fddb04c91012f5f7a83a5c90deb2470089d6e38cd5914b9c946eca6e9d31bbf8667d36cf87effc3f3ff283c21dd4137bd569fe7cf758feac94053e4baf7338bb592c8b7c291667fadf4a9bf9a2a154a18f612cbc7f851b3f8f2070e0a9d180622ee4f8e81b0ab250d504cef24116a3ff188cc829fcd8610b56343569e8dc997629410d1967ca9dd1d27eec5e01e4375aad16c46faba268524b154850d0d6fe3a76af2c6aa3e97647c51036049ac565370028d6a439a2672b6face56e1b171496c0722cfa22d9da631be359661617c5d5a2d286c5e19db9452c1e21a0107b6400debda2decb0c838f342dd017cdb2dccdf1fe97e3df3f881856b546997a3fed9e279c720145101567dd56be21688fed66bf9759e432a9aa89cbbd225d13cdea4ca05f7a45cfb6a682a3d5b1e18f7e6cf934fae5098108bae9058d05c3387a01d8d02a656d2bfff67e9f46b2d8a6aac28129e52efddf6e552214c3f8a45bc7a912cca9a7fec1d7d06412c6972cb9e3dc518983f56530b8bffe7f92c4b6eb47d4aef59fb513c4653a42de61bc17ad7728e7fc7590ff05a9e991de03f023d0aaf8688ed6170def5091c66576a424ac1cb", + "next_node_id": "0324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c" + }, + { + "alias": "Bob", + "privkey": "4242424242424242424242424242424242424242424242424242424242424242", + "onion_message": "0201031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f05560002536d53f93796cad550b6c68662dca41f7e8c221c31022c64dd1a627b2df3982b25eac261e88369cfc66e1e3b6d9829cb3dcd707046e68a7796065202a7904811bf2608c5611cf74c9eb5371c7eb1a4428bb39a041493e2a568ddb0b2482a6cc6711bc6116cef144ebf988073cb18d9dd4ce2d3aa9de91a7dc6d7c6f11a852024626e66b41ba1158055505dff9cb15aa51099f315564d9ee3ed6349665dc3e209eedf9b5805ee4f69d315df44c80e63d0e2efbdab60ec96f44a3447c6a6ddb1efb6aa4e072bde1dab974081646bfddf3b02daa2b83847d74dd336465e76e9b8fecc2b0414045eeedfc39939088a76820177dd1103c99939e659beb07197bab9f714b30ba8dc83738e9a6553a57888aaeda156c68933a2f4ff35e3f81135076b944ed9856acbfee9c61299a5d1763eadd14bf5eaf71304c8e165e590d7ecbcd25f1650bf5b6c2ad1823b2dc9145e168974ecf6a2273c94decff76d94bc6708007a17f22262d63033c184d0166c14f41b225a956271947aae6ce65890ed8f0d09c6ffe05ec02ee8b9de69d7077a0c5adeb813aabcc1ba8975b73ab06ddea5f4db3c23a1de831602de2b83f990d4133871a1a81e53f86393e6a7c3a7b73f0c099fa72afe26c3027bb9412338a19303bd6e6591c04fb4cde9b832b5f41ae199301ea8c303b5cef3aca599454273565de40e1148156d1f97c1aa9e58459ab318304075e034f5b7899c12587b86776a18a1da96b7bcdc22864fccc4c41538ebce92a6f054d53bf46770273a70e75fe0155cd6d2f2e937465b0825ce3123b8c206fac4c30478fa0f08a97ade7216dce11626401374993213636e93545a31f500562130f2feb04089661ad8c34d5a4cbd2e4e426f37cb094c786198a220a2646ecadc38c04c29ee67b19d662c209a7b30bfecc7fe8bf7d274de0605ee5df4db490f6d32234f6af639d3fce38a2801bcf8d51e9c090a6c6932355a83848129a378095b34e71cb8f51152dc035a4fe8e802fec8de221a02ba5afd6765ce570bef912f87357936ea0b90cb2990f56035e89539ec66e8dbd6ed50835158614096990e019c3eba3d7dd6a77147641c6145e8b17552cd5cf7cd163dd40b9eaeba8c78e03a2cd8c0b7997d6f56d35f38983a202b4eb8a54e14945c4de1a6dde46167e11708b7a5ff5cb9c0f7fc12fae49a012aa90bb1995c038130b749c48e6f1ffb732e92086def42af10fbc460d94abeb7b2fa744a5e9a491d62a08452be8cf2fdef573deedc1fe97098bce889f98200b26f9bb99da9aceddda6d793d8e0e44a2601ef4590cfbb5c3d0197aac691e3d31c20fd8e38764962ca34dabeb85df28feabaf6255d4d0df3d814455186a84423182caa87f9673df770432ad8fdfe78d4888632d460d36d2719e8fa8e4b4ca10d817c5d6bc44a8b2affab8c2ba53b8bf4994d63286c2fad6be04c28661162fa1a67065ecda8ba8c13aee4a8039f4f0110e0c0da2366f178d8903e19136dad6df9d8693ce71f3a270f9941de2a93d9b67bc516207ac1687bf6e00b29723c42c7d9c90df9d5e599dbeb7b73add0a6a2b7aba82f98ac93cb6e60494040445229f983a81c34f7f686d166dfc98ec23a6318d4a02a311ac28d655ea4e0f9c3014984f31e621ef003e98c373561d9040893feece2e0fa6cd2dd565e6fbb2773a2407cb2c3273c306cf71f427f2e551c4092e067cf9869f31ac7c6c80dd52d4f85be57a891a41e34be0d564e39b4af6f46b85339254a58b205fb7e10e7d0470ee73622493f28c08962118c23a1198467e72c4ae1cd482144b419247a5895975ea90d135e2a46ef7e5794a1551a447ff0a0d299b66a7f565cd86531f5e7af5408d85d877ce95b1df12b88b7d5954903a5296325ba478ba1e1a9d1f30a2d5052b2e2889bbd64f72c72bc71d8817288a2", + "next_node_id": "027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007" + }, + { + "alias": "Carol", + "privkey": "4343434343434343434343434343434343434343434343434343434343434343", + "onion_message": "020102b684babfd400c8dd48b367e9754b8021a3594a34dc94d7101776c7f6a86d0582055600029a77e8523162efa1f4208f4f2050cd5c386ddb6ce6d36235ea569d217ec52209fb85fdf7dbc4786c373eebdba0ddc184cfbe6da624f610e93f62c70f2c56be1090b926359969f040f932c03f53974db5656233bd60af375517d4323002937d784c2c88a564bcefe5c33d3fc21c26d94dfacab85e2e19685fd2ff4c543650958524439b6da68779459aee5ffc9dc543339acec73ff43be4c44ddcbe1c11d50e2411a67056ba9db7939d780f5a86123fdd3abd6f075f7a1d78ab7daf3a82798b7ec1e9f1345bc0d1e935098497067e2ae5a51ece396fcb3bb30871ad73aee51b2418b39f00c8e8e22be4a24f4b624e09cb0414dd46239de31c7be035f71e8da4f5a94d15b44061f46414d3f355069b5c5b874ba56704eb126148a22ec873407fe118972127e63ff80e682e410f297f23841777cec0517e933eaf49d7e34bd203266b42081b3a5193b51ccd34b41342bc67cf73523b741f5c012ba2572e9dda15fbe131a6ac2ff24dc2a7622d58b9f3553092cfae7fae3c8864d95f97aa49ec8edeff5d9f5782471160ee412d82ff6767030fc63eec6a93219a108cd41433834b26676a39846a944998796c79cd1cc460531b8ded659cedfd8aecefd91944f00476f1496daafb4ea6af3feacac1390ea510709783c2aa81a29de27f8959f6284f4684102b17815667cbb0645396ac7d542b878d90c42a1f7f00c4c4eedb2a22a219f38afadb4f1f562b6e000a94e75cc38f535b43a3c0384ccef127fde254a9033a317701c710b2b881065723486e3f4d3eea5e12f374a41565fe43fa137c1a252c2153dde055bb343344c65ad0529010ece29bbd405effbebfe3ba21382b94a60ac1a5ffa03f521792a67b30773cb42e862a8a02a8bbd41b842e115969c87d1ff1f8c7b5726b9f20772dd57fe6e4ea41f959a2a673ffad8e2f2a472c4c8564f3a5a47568dd75294b1c7180c500f7392a7da231b1fe9e525ea2d7251afe9ca52a17fe54a116cb57baca4f55b9b6de915924d644cba9dade4ccc01939d7935749c008bafc6d3ad01cd72341ce5ddf7a5d7d21cf0465ab7a3233433aef21f9acf2bfcdc5a8cc003adc4d82ac9d72b36eb74e05c9aa6ccf439ac92e6b84a3191f0764dd2a2e0b4cc3baa08782b232ad6ecd3ca6029bc08cc094aef3aebddcaddc30070cb6023a689641de86cfc6341c8817215a4650f844cd2ca60f2f10c6e44cfc5f23912684d4457bf4f599879d30b79bf12ef1ab8d34dddc15672b82e56169d4c770f0a2a7a960b1e8790773f5ff7fce92219808f16d061cc85e053971213676d28fb48925e9232b66533dbd938458eb2cc8358159df7a2a2e4cf87500ede2afb8ce963a845b98978edf26a6948d4932a6b95d022004556d25515fe158092ce9a913b4b4a493281393ca731e8d8e5a3449b9d888fc4e73ffcbb9c6d6d66e88e03cf6e81a0496ede6e4e4172b08c000601993af38f80c7f68c9d5fff9e0e215cff088285bf039ca731744efcb7825a272ca724517736b4890f47e306b200aa2543c363e2c9090bcf3cf56b5b86868a62471c7123a41740392fc1d5ab28da18dca66618e9af7b42b62b23aba907779e73ca03ec60e6ab9e0484b9cae6578e0fddb6386cb3468506bf6420298bf4a690947ab582255551d82487f271101c72e19e54872ab47eae144db66bc2f8194a666a5daec08d12822cb83a61946234f2dfdbd6ca7d8763e6818adee7b401fcdb1ac42f9df1ac5cc5ac131f2869013c8d6cd29d4c4e3d05bccd34ca83366d616296acf854fa05149bfd763a25b9938e96826a037fdcb85545439c76df6beed3bdbd01458f9cf984997cc4f0a7ac3cc3f5e1eeb59c09cadcf5a537f16e444149c8f17d4bdaef16c9fbabc5ef06eb0f0bf3a07a1beddfeacdaf1df5582d6dbd6bb808d6ab31bc22e5d7", + "next_node_id": "032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991" + }, + { + "alias": "Dave", + "privkey": "4444444444444444444444444444444444444444444444444444444444444444", + "onion_message": "0201025aaca62db7ce6b46386206ef9930daa32e979a35cb185a41cb951aa7d254b03c055600025550b2910294fa73bda99b9de9c851be9cbb481e23194a1743033630efba546b86e7d838d0f6e9cc0ed088dbf6889f0dceca3bfc745bd77d013a31311fa932a8bf1d28387d9ff521eabc651dee8f861fed609a68551145a451f017ec44978addeee97a423c08445531da488fd1ddc998e9cdbfcea59517b53fbf1833f0bbe6188dba6ca773a247220ec934010daca9cc185e1ceb136803469baac799e27a0d82abe53dc48a06a55d1f643885cc7894677dd20a4e4152577d1ba74b870b9279f065f9b340cedb3ca13b7df218e853e10ccd1b59c42a2acf93f489e170ee4373d30ab158b60fc20d3ba73a1f8c750951d69fb5b9321b968ddc8114936412346aff802df65516e1c09c51ef19849ff36c0199fd88c8bec301a30fef0c7cb497901c038611303f64e4174b5daf42832aa5586b84d2c9b95f382f4269a5d1bd4be898618dc78dfd451170f72ca16decac5b03e60702112e439cadd104fb3bbb3d5023c9b80823fdcd0a212a7e1aaa6eeb027adc7f8b3723031d135a09a979a4802788bb7861c6cc85501fb91137768b70aeab309b27b885686604ffc387004ac4f8c44b101c39bc0597ef7fd957f53fc5051f534b10eb3852100962b5e58254e5558689913c26ad6072ea41f5c5db10077cfc91101d4ae393be274c74297da5cc381cd88d54753aaa7df74b2f9da8d88a72bc9218fcd1f19e4ff4aace182312b9509c5175b6988f044c5756d232af02a451a02ca752f3c52747773acff6fd07d2032e6ce562a2c42105d106eba02d0b1904182cdc8c74875b082d4989d3a7e9f0e73de7c75d357f4af976c28c0b206c5e8123fc2391d078592d0d5ff686fd245c0a2de2e535b7cca99c0a37d432a8657393a9e3ca53eec1692159046ba52cb9bc97107349d8673f74cbc97e231f1108005c8d03e24ca813cea2294b39a7a493bcc062708f1f6cf0074e387e7d50e0666ce784ef4d31cb860f6cad767438d9ea5156ff0ae86e029e0247bf94df75ee0cda4f2006061455cb2eaff513d558863ae334cef7a3d45f55e7cc13153c6719e9901c1d4db6c03f643b69ea4860690305651794284d9e61eb848ccdf5a77794d376f0af62e46d4835acce6fd9eef5df73ebb8ea3bb48629766967f446e744ecc57ff3642c4aa1ccee9a2f72d5caa75fa05787d08b79408fce792485fdecdc25df34820fb061275d70b84ece540b0fc47b2453612be34f2b78133a64e812598fbe225fd85415f8ffe5340ce955b5fd9d67dd88c1c531dde298ed25f96df271558c812c26fa386966c76f03a6ebccbca49ac955916929bd42e134f982dde03f924c464be5fd1ba44f8dc4c3cbc8162755fd1d8f7dc044b15b1a796c53df7d8769bb167b2045b49cc71e08908796c92c16a235717cabc4bb9f60f8f66ff4fff1f9836388a99583acebdff4a7fb20f48eedcd1f4bdcc06ec8b48e35307df51d9bc81d38a94992dd135b30079e1f592da6e98dff496cb1a7776460a26b06395b176f585636ebdf7eab692b227a31d6979f5a6141292698e91346b6c806b90c7c6971e481559cae92ee8f4136f2226861f5c39ddd29bbdb118a35dece03f49a96804caea79a3dacfbf09d65f2611b5622de51d98e18151acb3bb84c09caaa0cc80edfa743a4679f37d6167618ce99e73362fa6f213409931762618a61f1738c071bba5afc1db24fe94afb70c40d731908ab9a505f76f57a7d40e708fd3df0efc5b7cbb2a7b75cd23449e09684a2f0e2bfa0d6176c35f96fe94d92fc9fa4103972781f81cb6e8df7dbeb0fc529c600d768bed3f08828b773d284f69e9a203459d88c12d6df7a75be2455fec128f07a497a2b2bf626cc6272d0419ca663e9dc66b8224227eb796f0246dcae9c5b0b6cfdbbd40c3245a610481c92047c968c9fc92c04b89cc41a0c15355a8f", + "tlvs": { + "unknown_tag_1": "68656c6c6f", + "encrypted_recipient_data": "bdc03f088764c6224c8f939e321bf096f363b2092db381fc8787f891c8e6dc9284991b98d2a63d9f91fe563065366dd406cd8e112cdaaa80d0e6" + } + } + ] + } +} From 5e26d465363d9c0eb4e857762d9f96a9ccbf85f9 Mon Sep 17 00:00:00 2001 From: Gijs van Dam Date: Wed, 25 Jun 2025 15:30:58 +0200 Subject: [PATCH 3/9] sphinx_test: add test for blinded route processing We add TestOnionMessageRouteBlinding which verifies that the onion message packet from the test vector can be processed correctly by the nodes in a blinded route. --- path_test.go | 135 +++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 131 insertions(+), 4 deletions(-) diff --git a/path_test.go b/path_test.go index 909b6bb..d97ab34 100644 --- a/path_test.go +++ b/path_test.go @@ -393,6 +393,129 @@ func TestOnionRouteBlinding(t *testing.T) { } } +// TestOnionMessageRouteBlinding tests that an onion message packet can +// correctly be processed by a node in a blinded route. +func TestOnionMessageRouteBlinding(t *testing.T) { + t.Parallel() + + // First, we'll read out the raw Json file at the target location. + jsonBytes, err := os.ReadFile(blindedOnionMessageOnionTestFileName) + require.NoError(t, err) + + // Once we have the raw file, we'll unpack it into our + // onionMessageJsonTestCase struct defined below. + testCase := &onionMessageJsonTestCase{} + require.NoError(t, json.Unmarshal(jsonBytes, testCase)) + + // Extract the original onion message packet to be processed. + onion, err := hex.DecodeString(testCase.OnionMessage.OnionMessagePacket) + require.NoError(t, err) + + onionBytes := bytes.NewReader(onion) + onionPacket := &OnionPacket{} + require.NoError(t, onionPacket.Decode(onionBytes)) + + // peelOnion is a helper closure that can be used to set up a Router + // and use it to process the given onion packet. + peelOnion := func(key *btcec.PrivateKey, + blindingPoint *btcec.PublicKey, + onionPacket *OnionPacket) *ProcessedPacket { + + r := NewRouter(&PrivKeyECDH{PrivKey: key}, NewMemoryReplayLog()) + + require.NoError(t, r.Start()) + defer r.Stop() + + // Onion messages don't use associated data, so we pass in nil + // here. Also, we set a random value for incomingCLTV as it's + // only used as an accompanying purposefully general type in the + // ReplayLog. + res, err := r.ProcessOnionPacket( + onionPacket, nil, 10, + WithBlindingPoint(blindingPoint), + ) + require.NoError(t, err) + + return res + } + + hops := testCase.Generate.Hops + + // There are some things that the processor of the onion will only be + // able to determine from the actual contents of the onion_message and + // the encrypted_recipient_date it receives. These things include the + // first_path_key for the introduction point and the + // next_path_key_override. This test doesn't decode the onion_message + // and the decryption of the encrypted_recipient_data so it doesn't + // extract these values. Instead we provide them manually. It also needs + // to know where the next_path_key_override is located in the route, + // hence it needs the concatIndex, where the part of the blinded route + // constructed by Dave starts. + var ( + firstPathKey = pubKeyFromString( + testCase.Route.FirstPathKey, + ) + concatIndex = 1 + nextPathKeyOverride = pubKeyFromString( + hops[0].EncodedOnionMessageTLVs.NextPathKeyOverride, + ) + ) + + // Onion message routes are always entirely blinded, so the first hop + // will always use the first path key. + pathKey := firstPathKey + + currentOnionPacket := onionPacket + for i, hop := range testCase.Decrypt.Hops { + // We encode the onion message packet to a buffer at each hop to + // compare it to the onion message packet in the test vector. + buff := bytes.NewBuffer(nil) + require.NoError(t, currentOnionPacket.Encode(buff)) + + // hop.OnionMessage contains the onion_message hex string. This + // contains the type 513 (two bytes), the path_key (33 bytes) + // and the length of the onion_message_packet (two bytes). We + // are only interested in the onion_message_packet so we only + // check that part. 2 + 33 + 2 = 37 bytes, so we skip the first + // 37 bytes, which equals 74 hex characters. + const onionMessageHexHeaderLen = 74 + + require.Equal( + t, hop.OnionMessage[onionMessageHexHeaderLen:], + hex.EncodeToString(buff.Bytes()), + ) + + priv := privKeyFromString(hop.PrivKey) + + if i == concatIndex { + pathKey = nextPathKeyOverride + } + + // With peelOnion we call into ProcessOnionPacket (with the + // functional option WithBlindingPoint) and we expect that the + // onion message packet for this hop is processed without error, + // otherwise peelOnion fails the test. + processedPkt := peelOnion( + priv, pathKey, currentOnionPacket, + ) + + // We derive the next path key from the current path key and the + // private key of the current hop. The new path key will be used + // to peel the next hop's onion unless it is overridden by a + // path key override. + pathKey, err = NextEphemeral( + &PrivKeyECDH{priv}, pathKey, + ) + require.NoError(t, err) + + // We set the current onion packet to the next packet in the + // processed packet. This is the packet that the next hop will + // process. During the next iteration we will run all the above + // checks on this packet. + currentOnionPacket = processedPkt.NextPacket + } +} + type onionBlindingJsonTestCase struct { Generate generateOnionData `json:"generate"` Decrypt decryptData `json:"decrypt"` @@ -432,10 +555,10 @@ type blindingJsonTestCase struct { } type onionMessageJsonTestCase struct { - Generate generateOnionMessageData `json:"generate"` - Route routeOnionMessageData `json:"route"` - // OnionMessage onionMessageData `json:"onionmessage"` - Decrypt decryptOnionMessageData `json:"decrypt"` + Generate generateOnionMessageData `json:"generate"` + Route routeOnionMessageData `json:"route"` + OnionMessage onionMessageData `json:"onionmessage"` + Decrypt decryptOnionMessageData `json:"decrypt"` } type routeData struct { @@ -450,6 +573,10 @@ type routeOnionMessageData struct { Hops []blindedOnionMessageHop `json:"hops"` } +type onionMessageData struct { + OnionMessagePacket string `json:"onion_message_packet"` +} + type unblindData struct { Hops []unblindedHop `json:"hops"` } From b9c2b35d82a079b8379439cfc462587b9c2faf44 Mon Sep 17 00:00:00 2001 From: Gijs van Dam Date: Fri, 25 Jul 2025 14:29:41 +0200 Subject: [PATCH 4/9] go.mod and .gitignore update --- .gitignore | 1 + go.mod | 25 +++++++++++--------- go.sum | 69 ++++++++++++++++++++++++++++-------------------------- 3 files changed, 51 insertions(+), 44 deletions(-) diff --git a/.gitignore b/.gitignore index 31e3ac6..033d67f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ vendor/ .idea +.aider* diff --git a/go.mod b/go.mod index 74cb6c6..4869baa 100644 --- a/go.mod +++ b/go.mod @@ -2,22 +2,25 @@ module github.com/lightningnetwork/lightning-onion require ( github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da - github.com/btcsuite/btcd v0.22.0-beta.0.20220207191057-4dc4ff7963b4 - github.com/btcsuite/btcd/btcec/v2 v2.1.0 - github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f + github.com/btcsuite/btcd v0.24.3-0.20250318170759-4f4ea81776d6 + github.com/btcsuite/btcd/btcec/v2 v2.3.4 + github.com/btcsuite/btclog v0.0.0-20241003133417-09c4e92e319c github.com/davecgh/go-spew v1.1.1 - github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 - github.com/stretchr/testify v1.8.2 - github.com/urfave/cli v1.22.5 - golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 + github.com/stretchr/testify v1.10.0 + github.com/urfave/cli v1.22.9 + golang.org/x/crypto v0.33.0 ) require ( - github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d // indirect + github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect + github.com/kr/pretty v0.3.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/russross/blackfriday/v2 v2.0.1 // indirect - github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect - golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed // indirect + github.com/rogpeppe/go-internal v1.13.1 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + golang.org/x/sys v0.30.0 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index af52f16..b3fdd5e 100644 --- a/go.sum +++ b/go.sum @@ -1,47 +1,50 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY= github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA= -github.com/btcsuite/btcd v0.22.0-beta.0.20220207191057-4dc4ff7963b4 h1:CEGr/598C/0LZQUoioaT6sdGGcJgu4+ck0PDeJ/QkKs= -github.com/btcsuite/btcd v0.22.0-beta.0.20220207191057-4dc4ff7963b4/go.mod h1:7alexyj/lHlOtr2PJK7L/+HDJZpcGDn/pAU98r7DY08= -github.com/btcsuite/btcd/btcec/v2 v2.1.0 h1:Whmbo9yShKKG+WrUfYGFfgj77vYBiwhwBSJnM66TMKI= -github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA= -github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f h1:bAs4lUbRJpnnkd9VhRV3jjAVU7DJVjMaK+IsvSeZvFo= -github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= -github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= +github.com/btcsuite/btcd v0.24.3-0.20250318170759-4f4ea81776d6 h1:8n9k3I7e8DkpdQ5YAP4j8ly/LSsbe6qX9vmVbrUGvVw= +github.com/btcsuite/btcd v0.24.3-0.20250318170759-4f4ea81776d6/go.mod h1:OmM4kFtB0klaG/ZqT86rQiyw/1iyXlJgc3UHClPhhbs= +github.com/btcsuite/btcd/btcec/v2 v2.3.4 h1:3EJjcN70HCu/mwqlUsGK8GcNVyLVxFDlWurTXGPFfiQ= +github.com/btcsuite/btcd/btcec/v2 v2.3.4/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04= +github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 h1:59Kx4K6lzOW5w6nFlA0v5+lk/6sjybR934QNHSJZPTQ= +github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= +github.com/btcsuite/btclog v0.0.0-20241003133417-09c4e92e319c h1:4HxD1lBUGUddhzgaNgrCPsFWd7cGYNpeFUgd9ZIgyM0= +github.com/btcsuite/btclog v0.0.0-20241003133417-09c4e92e319c/go.mod h1:w7xnGOhwT3lmrS4H3b/D1XAXxvh+tbhUm8xeHN2y3TQ= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/urfave/cli v1.22.5 h1:lNq9sAHXK2qfdI8W+GRItjCEkI+2oR4d+MEHy1CKXoU= -github.com/urfave/cli v1.22.5/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed h1:J22ig1FUekjjkmZUM7pTKixYm8DvrYsvrBZdunYeIuQ= -golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/urfave/cli v1.22.9 h1:cv3/KhXGBGjEXLC4bH0sLuJ9BewaAbpk5oyMOveu4pw= +github.com/urfave/cli v1.22.9/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= +golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From 4f2dbed30fb4b609f70ded378b6e8577479fa7e2 Mon Sep 17 00:00:00 2001 From: Gijs van Dam Date: Thu, 26 Jun 2025 18:21:29 +0200 Subject: [PATCH 5/9] sphinx_test: onion message packet creation TestTLVPayloadMessagePacket creates a onion message with payload and the blinded route from the test vector. It then checks if the onion packet we create is equal to the one provided in the test vector. --- sphinx_test.go | 140 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 140 insertions(+) diff --git a/sphinx_test.go b/sphinx_test.go index 8c84651..e4da3f2 100644 --- a/sphinx_test.go +++ b/sphinx_test.go @@ -4,6 +4,7 @@ import ( "bytes" "encoding/hex" "encoding/json" + "errors" "fmt" "io" "os" @@ -39,6 +40,58 @@ var ( testLegacyRouteNumHops = 20 ) +// Static errors used in tests. +var ( + // ErrInsufficientHops is returned when there are not enough hops to + // create a route. + ErrInsufficientHops = errors.New("at least 2 hops are required to " + + "create an onion message route") +) + +// encodeTLVRecord encodes a TLV record with the given type and value. +func encodeTLVRecord(recordType uint64, value []byte) []byte { + var buf bytes.Buffer + + // Encode type as varint + writeVarInt(&buf, recordType) + + // Encode length as varint + writeVarInt(&buf, uint64(len(value))) + + // Write value + buf.Write(value) + + return buf.Bytes() +} + +// writeVarInt writes a variable-length integer to the buffer. +func writeVarInt(buf *bytes.Buffer, n uint64) { + switch { + case n < 0xfd: + buf.WriteByte(byte(n)) + case n <= 0xffff: + buf.WriteByte(0xfd) + buf.WriteByte(byte(n)) + buf.WriteByte(byte(n >> 8)) + case n <= 0xffffffff: + buf.WriteByte(0xfe) + buf.WriteByte(byte(n)) + buf.WriteByte(byte(n >> 8)) + buf.WriteByte(byte(n >> 16)) + buf.WriteByte(byte(n >> 24)) + default: + buf.WriteByte(0xff) + buf.WriteByte(byte(n)) + buf.WriteByte(byte(n >> 8)) + buf.WriteByte(byte(n >> 16)) + buf.WriteByte(byte(n >> 24)) + buf.WriteByte(byte(n >> 32)) + buf.WriteByte(byte(n >> 40)) + buf.WriteByte(byte(n >> 48)) + buf.WriteByte(byte(n >> 56)) + } +} + func newTestRoute(numHops int) ([]*Router, *PaymentPath, *[]HopData, *OnionPacket, error) { nodes := make([]*Router, numHops) @@ -162,6 +215,90 @@ func TestBolt4Packet(t *testing.T) { } } +// TestTLVPayloadMessagePacket tests the creation and encoding of an onion +// message packet that uses a TLV payload for each hop in the route. This test +// uses the test vectors defined in the BOLT 4 specification. The test reads a +// JSON file containing a predefined route, session key, and the expected final +// onion packet. It then constructs the route hop-by-hop, manually creating the +// TLV payload for each, before creating a new onion packet with NewOnionPacket. +// The test concludes by asserting that the newly encoded packet is identical to +// the one specified in the test vector. +func TestTLVPayloadMessagePacket(t *testing.T) { + t.Parallel() + + // First, we'll read out the raw JSON file at the target location. + jsonBytes, err := os.ReadFile(testOnionMessageFileName) + require.NoError(t, err) + + // Once we have the raw file, we'll unpack it into our + // onionMessageJsonTestCase struct defined in path_test.go. + testCase := &onionMessageJsonTestCase{} + require.NoError(t, json.Unmarshal(jsonBytes, testCase)) + + // Next, we'll populate a new OnionHop using the information included + // in this test case. + var route PaymentPath + for i, hop := range testCase.Route.Hops { + blindedPKbytes, err := hex.DecodeString(hop.BlindedNodeID) + require.NoError(t, err) + + blindedPubKey, err := btcec.ParsePubKey(blindedPKbytes) + require.NoError(t, err) + + encryptedRecipientData, err := hex.DecodeString( + hop.EncryptedRecipientData, + ) + require.NoError(t, err) + + // Manually encode our onion payload + var b bytes.Buffer + + if i == len(testCase.Route.Hops)-1 { + helloBytes := []byte("hello") + // Encode TLV record for type 1 (hello message) + b.Write(encodeTLVRecord(1, helloBytes)) + } + + // Encode TLV record for type 4 (encrypted recipient data) + b.Write(encodeTLVRecord(4, encryptedRecipientData)) + + route[i] = OnionHop{ + NodePub: *blindedPubKey, + HopPayload: HopPayload{ + // Onion messages always use TLV payloads. + Type: PayloadTLV, + Payload: b.Bytes(), + }, + } + } + + finalPacket, err := hex.DecodeString( + testCase.OnionMessage.OnionMessagePacket, + ) + require.NoError(t, err) + + sessionKeyBytes, err := hex.DecodeString(testCase.Generate.SessionKey) + + require.NoError(t, err) + + // With all the required data assembled, we'll craft a new packet. + sessionKey, _ := btcec.PrivKeyFromBytes(sessionKeyBytes) + + pkt, err := NewOnionPacket( + &route, sessionKey, nil, DeterministicPacketFiller, + ) + require.NoError(t, err) + + var b bytes.Buffer + require.NoError(t, pkt.Encode(&b)) + + // Finally, we expect that our packet matches the packet included in + // the spec's test vectors. + require.Equalf(t, finalPacket, b.Bytes(), "final packet does not "+ + "match expected BOLT 4 packet, want: %s, got %s", + hex.EncodeToString(finalPacket), hex.EncodeToString(b.Bytes())) +} + func TestSphinxCorrectness(t *testing.T) { nodes, _, hopDatas, fwdMsg, err := newTestRoute(testLegacyRouteNumHops) if err != nil { @@ -755,6 +892,9 @@ const ( // testTLVFileName is the name of the tlv-payload-only onion test file. testTLVFileName = "testdata/onion-test.json" + + // testOnionMessageFileName is the name of the onion message test file. + testOnionMessageFileName = "testdata/blinded-onion-message-onion-test.json" //nolint:lll ) type jsonHop struct { From 77d81d838ca632816ddfeb73d7a3025290f3d6f1 Mon Sep 17 00:00:00 2001 From: Gijs van Dam Date: Tue, 8 Jul 2025 16:48:50 +0200 Subject: [PATCH 6/9] multi: decode zero-length onion message payloads Since the onion message payload can be zero-length, we need to decode it correctly. This commit adds a boolean flag to the HopPayload Decode that tells whether the payload is an onion message payload or not. If it is, the payload is decoded as a tlv payload also if the first byte is 0x00. sphinx_test: Add zero-length payload om test --- error.go | 8 ++++- payload.go | 96 +++++++++++++++++++++++++++++++++++--------------- sphinx.go | 44 ++++++++++++++++------- sphinx_test.go | 56 +++++++++++++++++++++++++++++ 4 files changed, 161 insertions(+), 43 deletions(-) diff --git a/error.go b/error.go index a32c999..72fb425 100644 --- a/error.go +++ b/error.go @@ -1,6 +1,9 @@ package sphinx -import "fmt" +import ( + "errors" + "fmt" +) var ( // ErrReplayedPacket is an error returned when a packet is rejected @@ -24,4 +27,7 @@ var ( // ErrLogEntryNotFound is an error returned when a packet lookup in a replay // log fails because it is missing. ErrLogEntryNotFound = fmt.Errorf("sphinx packet is not in log") + + // ErrIOReadFull is returned when an io read full operation fails. + ErrIOReadFull = errors.New("io read full error") ) diff --git a/payload.go b/payload.go index 9e89dad..e6040de 100644 --- a/payload.go +++ b/payload.go @@ -87,48 +87,62 @@ func (hp *HopPayload) Encode(w io.Writer) error { } // Decode unpacks an encoded HopPayload from the passed reader into the target -// HopPayload. -func (hp *HopPayload) Decode(r io.Reader) error { +// HopPayload. tlvGuaranteed should be set to true if the caller only wishes to +// accept TLV encoded payloads. By doing so, zero-lengt tlv payloads are +// supported. If set to false, then the function will inspect the first byte to +// determine the type of payload. +func DecodeHopPayload(r io.Reader, tlvGuaranteed bool) (*HopPayload, error) { bufReader := bufio.NewReader(r) - // In order to properly parse the payload, we'll need to check the - // first byte. We'll use a bufio reader to peek at it without consuming - // it from the buffer. + var payloadSize uint16 + + hopPayload := &HopPayload{} + + // If we are not sure if this is a TLV or legacy payload, then we need + // to inspect the first byte to determine the type of payload. The first + // byte is either a realm (legacy) or the beginning of a var-int + // encoding the length of the payload (TLV). We'll use a bufio reader to + // peek at it without consuming it from the buffer. peekByte, err := bufReader.Peek(1) if err != nil { - return err + return nil, fmt.Errorf("peek first payload byte: %w", err) } - var ( - legacyPayload = isLegacyPayloadByte(peekByte[0]) - payloadSize uint16 - ) + switch { + case tlvGuaranteed: + // If we're instructed to only accept TLV payloads, then we set + // the type accordingly. This allows us to support zero-length + // TLV payloads. + + hopPayload.Type = PayloadTLV - if legacyPayload { - payloadSize = legacyPayloadSize() - hp.Type = PayloadLegacy - } else { payloadSize, err = tlvPayloadSize(bufReader) if err != nil { - return err + return nil, err } - hp.Type = PayloadTLV - } + case isLegacyPayloadByte(peekByte[0]): + // If the first byte indicates that this is a legacy payload, + // then we set the type accordingly. + hopPayload.Type = PayloadLegacy + payloadSize = legacyPayloadSize() - // Now that we know the payload size, we'll create a new buffer to - // read it out in full. - // - // TODO(roasbeef): can avoid all these copies - hp.Payload = make([]byte, payloadSize) - if _, err := io.ReadFull(bufReader, hp.Payload[:]); err != nil { - return err + default: + // Otherwise, we set the type to TLV. + hopPayload.Type = PayloadTLV + + payloadSize, err = tlvPayloadSize(bufReader) + if err != nil { + return nil, err + } } - if _, err := io.ReadFull(bufReader, hp.HMAC[:]); err != nil { - return err + + err = readPayloadAndHMAC(hopPayload, bufReader, payloadSize) + if err != nil { + return nil, err } - return nil + return hopPayload, nil } // HopData attempts to extract a set of forwarding instructions from the target @@ -146,6 +160,26 @@ func (hp *HopPayload) HopData() (*HopData, error) { return nil, nil } +// readPayloadAndHMAC reads the payload and HMAC from the reader into the +// HopPayload. +func readPayloadAndHMAC(hp *HopPayload, r io.Reader, payloadSize uint16) error { + // Now that we know the payload size, we'll create a new buffer to read + // it out in full. + hp.Payload = make([]byte, payloadSize) + + _, err := io.ReadFull(r, hp.Payload) + if err != nil { + return fmt.Errorf("%w: %w", ErrIOReadFull, err) + } + + _, err = io.ReadFull(r, hp.HMAC[:]) + if err != nil { + return fmt.Errorf("%w: %w", ErrIOReadFull, err) + } + + return nil +} + // tlvPayloadSize uses the passed reader to extract the payload length encoded // as a var-int. func tlvPayloadSize(r io.Reader) (uint16, error) { @@ -314,8 +348,12 @@ func legacyNumBytes() int { return LegacyHopDataSize } -// isLegacyPayload returns true if the given byte is equal to the 0x00 byte -// which indicates that the payload should be decoded as a legacy payload. +// isLegacyPayloadByte determines if the first byte of a hop payload indicates +// that it is a legacy payload. The first byte of a legacy payload will always +// be 0x00, as this is the realm. For TLV payloads, the first byte is a +// var-int encoding the length of the payload. A TLV stream can be empty, in +// which case its length is 0, which is also encoded as a 0x00 byte. This +// creates an ambiguity between a legacy payload and an empty TLV payload. func isLegacyPayloadByte(b byte) bool { return b == 0x00 } diff --git a/sphinx.go b/sphinx.go index 8e16b23..bc0cff9 100644 --- a/sphinx.go +++ b/sphinx.go @@ -510,7 +510,8 @@ func (r *Router) Stop() { // processOnionCfg is a set of config values that can be used to modify how an // onion is processed. type processOnionCfg struct { - blindingPoint *btcec.PublicKey + blindingPoint *btcec.PublicKey + tlvPayloadOnly bool } // ProcessOnionOpt defines the signature of a function option that can be used @@ -525,6 +526,14 @@ func WithBlindingPoint(point *btcec.PublicKey) ProcessOnionOpt { } } +// WithTLVPayloadOnly is a functional option that signals that the onion packet +// being processed is an onion_message_packet. +func WithTLVPayloadOnly() ProcessOnionOpt { + return func(cfg *processOnionCfg) { + cfg.tlvPayloadOnly = true + } +} + // ProcessOnionPacket processes an incoming onion packet which has been forward // to the target Sphinx router. If the encoded ephemeral key isn't on the // target Elliptic Curve, then the packet is rejected. Similarly, if the @@ -560,7 +569,9 @@ func (r *Router) ProcessOnionPacket(onionPkt *OnionPacket, assocData []byte, // Continue to optimistically process this packet, deferring replay // protection until the end to reduce the penalty of multiple IO // operations. - packet, err := processOnionPacket(onionPkt, &sharedSecret, assocData) + packet, err := processOnionPacket( + onionPkt, &sharedSecret, assocData, cfg.tlvPayloadOnly, + ) if err != nil { return nil, err } @@ -594,7 +605,9 @@ func (r *Router) ReconstructOnionPacket(onionPkt *OnionPacket, assocData []byte, return nil, err } - return processOnionPacket(onionPkt, &sharedSecret, assocData) + return processOnionPacket( + onionPkt, &sharedSecret, assocData, cfg.tlvPayloadOnly, + ) } // DecryptBlindedHopData uses the router's private key to decrypt data encrypted @@ -625,7 +638,8 @@ func (r *Router) OnionPublicKey() *btcec.PublicKey { // packet. This function returns the next inner onion packet layer, along with // the hop data extracted from the outer onion packet. func unwrapPacket(onionPkt *OnionPacket, sharedSecret *Hash256, - assocData []byte) (*OnionPacket, *HopPayload, error) { + assocData []byte, tlvPayloadOnly bool) (*OnionPacket, *HopPayload, + error) { dhKey := onionPkt.EphemeralKey routeInfo := onionPkt.RoutingInfo @@ -649,8 +663,8 @@ func unwrapPacket(onionPkt *OnionPacket, sharedSecret *Hash256, zeroBytes := bytes.Repeat([]byte{0}, MaxPayloadSize) headerWithPadding := append(routeInfo[:], zeroBytes...) - var hopInfo [numStreamBytes]byte - xor(hopInfo[:], headerWithPadding, streamBytes) + hopInfo := make([]byte, numStreamBytes) + xor(hopInfo, headerWithPadding, streamBytes) // Randomize the DH group element for the next hop using the // deterministic blinding factor. @@ -660,8 +674,10 @@ func unwrapPacket(onionPkt *OnionPacket, sharedSecret *Hash256, // With the MAC checked, and the payload decrypted, we can now parse // out the payload so we can derive the specified forwarding // instructions. - var hopPayload HopPayload - if err := hopPayload.Decode(bytes.NewReader(hopInfo[:])); err != nil { + hopPayload, err := DecodeHopPayload( + bytes.NewReader(hopInfo), tlvPayloadOnly, + ) + if err != nil { return nil, nil, err } @@ -676,14 +692,14 @@ func unwrapPacket(onionPkt *OnionPacket, sharedSecret *Hash256, HeaderMAC: hopPayload.HMAC, } - return innerPkt, &hopPayload, nil + return innerPkt, hopPayload, nil } // processOnionPacket performs the primary key derivation and handling of onion // packets. The processed packets returned from this method should only be used // if the packet was not flagged as a replayed packet. func processOnionPacket(onionPkt *OnionPacket, sharedSecret *Hash256, - assocData []byte) (*ProcessedPacket, error) { + assocData []byte, tlvPayloadOnly bool) (*ProcessedPacket, error) { // First, we'll unwrap an initial layer of the onion packet. Typically, // we'll only have a single layer to unwrap, However, if the sender has @@ -693,7 +709,7 @@ func processOnionPacket(onionPkt *OnionPacket, sharedSecret *Hash256, // they can properly check the HMAC and unwrap a layer for their // handoff hop. innerPkt, outerHopPayload, err := unwrapPacket( - onionPkt, sharedSecret, assocData, + onionPkt, sharedSecret, assocData, tlvPayloadOnly, ) if err != nil { return nil, err @@ -703,7 +719,7 @@ func processOnionPacket(onionPkt *OnionPacket, sharedSecret *Hash256, // However if the uncovered 'nextMac' is all zeroes, then this // indicates that we're the final hop in the route. var action ProcessCode = MoreHops - if bytes.Compare(zeroHMAC[:], outerHopPayload.HMAC[:]) == 0 { + if bytes.Equal(zeroHMAC[:], outerHopPayload.HMAC[:]) { action = ExitNode } @@ -794,7 +810,9 @@ func (t *Tx) ProcessOnionPacket(seqNum uint16, onionPkt *OnionPacket, // Continue to optimistically process this packet, deferring replay // protection until the end to reduce the penalty of multiple IO // operations. - packet, err := processOnionPacket(onionPkt, &sharedSecret, assocData) + packet, err := processOnionPacket( + onionPkt, &sharedSecret, assocData, cfg.tlvPayloadOnly, + ) if err != nil { return err } diff --git a/sphinx_test.go b/sphinx_test.go index e4da3f2..ed4ea1c 100644 --- a/sphinx_test.go +++ b/sphinx_test.go @@ -299,6 +299,62 @@ func TestTLVPayloadMessagePacket(t *testing.T) { hex.EncodeToString(finalPacket), hex.EncodeToString(b.Bytes())) } +// TestProcessOnionMessageZeroLengthPayload tests that we can properly process +// an onion message that has a zero-length payload. +func TestProcessOnionMessageZeroLengthPayload(t *testing.T) { + t.Parallel() + + // First, create a router that will be the destination of the onion + // message. + privKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + router := NewRouter(&PrivKeyECDH{privKey}, NewMemoryReplayLog()) + err = router.Start() + require.NoError(t, err) + + defer router.Stop() + + // Next, create a session key for the onion packet. + sessionKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + // We'll create a simple one-hop path. + path := &PaymentPath{ + { + NodePub: *privKey.PubKey(), + }, + } + + // The hop payload will be an empty TLV payload. + payload, err := NewTLVHopPayload(nil) + require.NoError(t, err) + require.Empty(t, payload.Payload) + path[0].HopPayload = payload + + // Now, create the onion packet. + onionPacket, err := NewOnionPacket( + path, sessionKey, nil, DeterministicPacketFiller, + ) + require.NoError(t, err) + + // We'll now process the packet, making sure to indicate that this is + // an onion message. + processedPacket, err := router.ProcessOnionPacket( + onionPacket, nil, 0, WithTLVPayloadOnly(), + ) + require.NoError(t, err) + + // The packet should be decoded as an exit node. + require.EqualValues(t, ExitNode, processedPacket.Action) + + // The payload should be of type TLV. + require.Equal(t, PayloadTLV, processedPacket.Payload.Type) + + // And the payload should be empty. + require.Empty(t, processedPacket.Payload.Payload) +} + func TestSphinxCorrectness(t *testing.T) { nodes, _, hopDatas, fwdMsg, err := newTestRoute(testLegacyRouteNumHops) if err != nil { From 148f914034f6d876aeef6df231caf8bd3ffe4883 Mon Sep 17 00:00:00 2001 From: Gijs van Dam Date: Tue, 8 Jul 2025 17:47:54 +0200 Subject: [PATCH 7/9] multi: Support custom size onion packets Onion messages allow for payloads that exceed 1300 bytes, in which case the payload should become 32768 bytes. This commit introduces support for those custom size packets and the tests for this feature. NewOnionPacket now allows for a final variadic argument payloadSizes. The sizes passed are then compared to the actual payload size of the entire path, and the first value that fits the actual payload size will then be used as the size of the routing info. We use this to fix the size of onion messages at 1300 or 32768 bytes as suggested by BOLT-0004 but it can be used to fix the size at any value. If no values are passed the func defaults to MaxRoutingPayloadSize. MaxRoutingPayloadSize and MaxOnionMessagePayloadSize are exposed to facilitate easy usage of this library. sphinx_test now has a helper function to create onion messages of a specified length. This helper is then used to test the handling of packets larger than 1300 bytes specifically for onion messages. --- cmd/main.go | 9 ++ error.go | 32 ++++- packetfiller.go | 14 ++- sphinx.go | 189 +++++++++++++++++++--------- sphinx_test.go | 321 ++++++++++++++++++++++++++++++++++++++++++++++-- 5 files changed, 489 insertions(+), 76 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index 032738f..9087d73 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -70,6 +70,13 @@ func main() { "data.", Value: defaultHopDataPath, }, + cli.IntFlag{ + Name: "payload-size", + Usage: "The size for a payload for a " + + "single hop. Defaults to the " + + "max routing payload size", + Value: sphinx.MaxRoutingPayloadSize, + }, }, }, { @@ -203,8 +210,10 @@ func generate(ctx *cli.Context) error { return fmt.Errorf("could not peel onion spec: %v", err) } + payloadSize := ctx.Int("payload-size") msg, err := sphinx.NewOnionPacket( path, sessionKey, assocData, sphinx.DeterministicPacketFiller, + sphinx.WithMaxPayloadSize(payloadSize), ) if err != nil { return fmt.Errorf("error creating message: %v", err) diff --git a/error.go b/error.go index 72fb425..3a9f312 100644 --- a/error.go +++ b/error.go @@ -24,9 +24,35 @@ var ( ErrInvalidOnionKey = fmt.Errorf("invalid onion key: pubkey isn't on " + "secp256k1 curve") - // ErrLogEntryNotFound is an error returned when a packet lookup in a replay - // log fails because it is missing. - ErrLogEntryNotFound = fmt.Errorf("sphinx packet is not in log") + // ErrLogEntryNotFound is an error returned when a packet lookup in a + // replay log fails because it is missing. + ErrLogEntryNotFound = errors.New("sphinx packet is not in log") + + // ErrPayloadSizeExceeded is returned when the payload size exceeds the + // configured payload size of the onion packet. + ErrPayloadSizeExceeded = errors.New("max payload size exceeded") + + // ErrSharedSecretDerivation is returned when we fail to derive the + // shared secret for a hop. + ErrSharedSecretDerivation = errors.New("error generating shared secret") + + // ErrMissingHMAC is returned when the onion packet is too small to + // contain a valid HMAC. + ErrMissingHMAC = errors.New("onion packet is too small, missing HMAC") + + // ErrNegativeRoutingInfoSize is returned when a negative routing info + // size is specified in the Sphinx configuration. + ErrNegativeRoutingInfoSize = errors.New("routing info size must be " + + "non-negative") + + // ErrNegativePayloadSize is returned when a negative payload size is + // specified in the Sphinx configuration. + ErrNegativePayloadSize = errors.New("payload size must be " + + "non-negative") + + // ErrZeroHops is returned when attempting to create a route with zero + // hops. + ErrZeroHops = errors.New("route of length zero passed in") // ErrIOReadFull is returned when an io read full operation fails. ErrIOReadFull = errors.New("io read full error") diff --git a/packetfiller.go b/packetfiller.go index 79c1441..9bcf3df 100644 --- a/packetfiller.go +++ b/packetfiller.go @@ -12,16 +12,17 @@ import ( // in order to ensure we don't leak information on the true route length to the // receiver. The packet filler may also use the session key to generate a set // of filler bytes if it wishes to be deterministic. -type PacketFiller func(*btcec.PrivateKey, *[routingInfoSize]byte) error +type PacketFiller func(*btcec.PrivateKey, []byte) error // RandPacketFiller is a packet filler that reads a set of random bytes from a // CSPRNG. -func RandPacketFiller(_ *btcec.PrivateKey, mixHeader *[routingInfoSize]byte) error { +func RandPacketFiller(_ *btcec.PrivateKey, mixHeader []byte) error { // Read out random bytes to fill out the rest of the starting packet // after the hop payload for the final node. This mitigates a privacy // leak that may reveal a lower bound on the true path length to the // receiver. - if _, err := rand.Read(mixHeader[:]); err != nil { + _, err := rand.Read(mixHeader) + if err != nil { return err } @@ -31,7 +32,7 @@ func RandPacketFiller(_ *btcec.PrivateKey, mixHeader *[routingInfoSize]byte) err // BlankPacketFiller is a packet filler that doesn't attempt to fill out the // packet at all. It should ONLY be used for generating test vectors or other // instances that required deterministic packet generation. -func BlankPacketFiller(_ *btcec.PrivateKey, _ *[routingInfoSize]byte) error { +func BlankPacketFiller(_ *btcec.PrivateKey, _ []byte) error { return nil } @@ -39,7 +40,7 @@ func BlankPacketFiller(_ *btcec.PrivateKey, _ *[routingInfoSize]byte) error { // set of filler bytes by using chacha20 with a key derived from the session // key. func DeterministicPacketFiller(sessionKey *btcec.PrivateKey, - mixHeader *[routingInfoSize]byte) error { + mixHeader []byte) error { // First, we'll generate a new key that'll be used to generate some // random bytes for our padding purposes. To derive this new key, we @@ -55,7 +56,8 @@ func DeterministicPacketFiller(sessionKey *btcec.PrivateKey, if err != nil { return err } - padCipher.XORKeyStream(mixHeader[:], mixHeader[:]) + + padCipher.XORKeyStream(mixHeader, mixHeader) return nil } diff --git a/sphinx.go b/sphinx.go index bc0cff9..9690154 100644 --- a/sphinx.go +++ b/sphinx.go @@ -6,6 +6,7 @@ import ( "crypto/sha256" "fmt" "io" + "slices" "sync" "github.com/btcsuite/btcd/btcec/v2" @@ -41,26 +42,16 @@ const ( LegacyHopDataSize = (RealmByteSize + AddressSize + AmtForwardSize + OutgoingCLTVSize + NumPaddingBytes + HMACSize) - // MaxPayloadSize is the maximum size a payload for a single hop can be. - // This is the worst case scenario of a single hop, consuming all - // available space. We need to know this in order to generate a - // sufficiently long stream of pseudo-random bytes when - // encrypting/decrypting the payload. - MaxPayloadSize = routingInfoSize - - // routingInfoSize is the fixed size of the the routing info. This - // consists of a addressSize byte address and a HMACSize byte HMAC for - // each hop of the route, the first pair in cleartext and the following - // pairs increasingly obfuscated. If not all space is used up, the - // remainder is padded with null-bytes, also obfuscated. - routingInfoSize = 1300 - - // numStreamBytes is the number of bytes produced by our CSPRG for the - // key stream implementing our stream cipher to encrypt/decrypt the mix - // header. The MaxPayloadSize bytes at the end are used to - // encrypt/decrypt the fillers when processing the packet of generating - // the HMACs when creating the packet. - numStreamBytes = routingInfoSize * 2 + // MaxRoutingPayloadSize is the maximum size an `update_add_htlc` + // payload for a single hop can be. This is the worst case scenario of a + // single hop, consuming all available space. We need to know this in + // order to generate a sufficiently long stream of pseudo-random bytes + // when encrypting/decrypting the payload. + MaxRoutingPayloadSize = 1300 + + // MaxOnionMessagePayloadSize is the size of the routing info for a + // onion messaging jumbo onion packet. + MaxOnionMessagePayloadSize = 32768 // keyLen is the length of the keys used to generate cipher streams and // encrypt payloads. Since we use SHA256 to generate the keys, the @@ -69,11 +60,11 @@ const ( // baseVersion represent the current supported version of onion packet. baseVersion = 0 -) -var ( - ErrMaxRoutingInfoSizeExceeded = fmt.Errorf( - "max routing info size of %v bytes exceeded", routingInfoSize) + // streamBytesMultiplier is the multiplier used to calculate the number + // of bytes that needs to be produced by our CSPRNG for the key stream + // implementing our stream cipher. + streamBytesMultiplier = 2 ) // OnionPacket is the onion wrapped hop-to-hop routing information necessary to @@ -102,7 +93,7 @@ type OnionPacket struct { // RoutingInfo is the full routing information for this onion packet. // This encodes all the forwarding instructions for this current hop // and all the hops in the route. - RoutingInfo [routingInfoSize]byte + RoutingInfo []byte // HeaderMAC is an HMAC computed with the shared secret of the routing // data and the associated data for this route. Including the @@ -190,14 +181,47 @@ func generateSharedSecrets(paymentPath []*btcec.PublicKey, return hopSharedSecrets, lastEphemeralPubKey, nil } +type newOnionPacketCfg struct { + payloadSize int +} + +// NewOnionPacketOpt defines the signature of a function option that can be used +// with NewOnionPacket. +type NewOnionPacketOpt func(cfg *newOnionPacketCfg) + +// WithMaxPayloadSize is a function option that can be used to set the maximum +// payload size. NewOnionPacket will default to MaxRoutingPayloadSize if this +// function option is not specified. For larger payloads, e.g., for large onion +// messages, it's suggested to use MaxOnionMessagePayloadSize, to keep the +// anonymity as big as possible by using one (large) fixed size. +func WithMaxPayloadSize(size int) NewOnionPacketOpt { + return func(cfg *newOnionPacketCfg) { + cfg.payloadSize = size + } +} + // NewOnionPacket creates a new onion packet which is capable of obliviously -// routing a message through the mix-net path outline by 'paymentPath'. +// routing a message through the mix-net path outline by 'paymentPath'. The +// total size of the onion 'clicks' to the first value in payloadSizes that is +// bigger than the total payload size of the path. If no size is given, it +// defaults to the maximum routing payload size. func NewOnionPacket(paymentPath *PaymentPath, sessionKey *btcec.PrivateKey, - assocData []byte, pktFiller PacketFiller) (*OnionPacket, error) { + assocData []byte, pktFiller PacketFiller, + opts ...NewOnionPacketOpt) (*OnionPacket, error) { + + cfg := &newOnionPacketCfg{} + for _, o := range opts { + o(cfg) + } - // Check whether total payload size doesn't exceed the hard maximum. - if paymentPath.TotalPayloadSize() > routingInfoSize { - return nil, ErrMaxRoutingInfoSizeExceeded + if cfg.payloadSize < 0 { + return nil, ErrNegativePayloadSize + } + + // We default to the maximum routing payload size if the caller didn't + // provide any payload sizes. + if cfg.payloadSize == 0 { + cfg.payloadSize = MaxRoutingPayloadSize } // If we don't actually have a partially populated route, then we'll @@ -207,6 +231,16 @@ func NewOnionPacket(paymentPath *PaymentPath, sessionKey *btcec.PrivateKey, return nil, fmt.Errorf("route of length zero passed in") } + totalPayloadSize := paymentPath.TotalPayloadSize() + + // Return an error if the actual payload size exceeds the configured + // payload size. + if totalPayloadSize > cfg.payloadSize { + return nil, fmt.Errorf("payload size %v, payload limit %v: %w", + totalPayloadSize, cfg.payloadSize, + ErrPayloadSizeExceeded) + } + // We'll force the caller to provide a packet filler, as otherwise we // may default to an insecure filling method (which should only really // be used to generate test vectors). @@ -218,22 +252,28 @@ func NewOnionPacket(paymentPath *PaymentPath, sessionKey *btcec.PrivateKey, paymentPath.NodeKeys(), sessionKey, ) if err != nil { - return nil, fmt.Errorf("error generating shared secret: %v", err) + return nil, fmt.Errorf("%w: %w", ErrSharedSecretDerivation, err) } // Generate the padding, called "filler strings" in the paper. - filler := generateHeaderPadding("rho", paymentPath, hopSharedSecrets) + filler, err := generateHeaderPadding( + "rho", paymentPath, hopSharedSecrets, cfg.payloadSize, + ) + if err != nil { + return nil, err + } // Allocate zero'd out byte slices to store the final mix header packet // and the hmac for each hop. var ( - mixHeader [routingInfoSize]byte + mixHeader = make([]byte, cfg.payloadSize) nextHmac [HMACSize]byte hopPayloadBuf bytes.Buffer ) // Fill the packet using the caller specified methodology. - if err := pktFiller(sessionKey, &mixHeader); err != nil { + err = pktFiller(sessionKey, mixHeader) + if err != nil { return nil, err } @@ -254,26 +294,28 @@ func NewOnionPacket(paymentPath *PaymentPath, sessionKey *btcec.PrivateKey, // Next, using the key dedicated for our stream cipher, we'll // generate enough bytes to obfuscate this layer of the onion // packet. - streamBytes := generateCipherStream(rhoKey, routingInfoSize) + streamBytes := generateCipherStream( + rhoKey, uint(cfg.payloadSize), + ) payload := paymentPath[i].HopPayload // Before we assemble the packet, we'll shift the current // mix-header to the right in order to make room for this next // per-hop data. shiftSize := payload.NumBytes() - rightShift(mixHeader[:], shiftSize) + rightShift(mixHeader, shiftSize) err := payload.Encode(&hopPayloadBuf) if err != nil { return nil, err } - copy(mixHeader[:], hopPayloadBuf.Bytes()) + copy(mixHeader, hopPayloadBuf.Bytes()) // Once the packet for this hop has been assembled, we'll // re-encrypt the packet by XOR'ing with a stream of bytes // generated using our shared secret. - xor(mixHeader[:], mixHeader[:], streamBytes[:]) + xor(mixHeader, mixHeader, streamBytes) // If this is the "last" hop, then we'll override the tail of // the hop data. @@ -285,7 +327,7 @@ func NewOnionPacket(paymentPath *PaymentPath, sessionKey *btcec.PrivateKey, // calculating the MAC, we'll also include the optional // associated data which can allow higher level applications to // prevent replay attacks. - packet := append(mixHeader[:], assocData...) + packet := slices.Concat(mixHeader, assocData) nextHmac = calcMac(muKey, packet) hopPayloadBuf.Reset() @@ -322,7 +364,9 @@ func rightShift(slice []byte, num int) { // leaving only the original "filler" bytes produced by this function at the // last hop. Using this methodology, the size of the field stays constant at // each hop. -func generateHeaderPadding(key string, path *PaymentPath, sharedSecrets []Hash256) []byte { +func generateHeaderPadding(key string, path *PaymentPath, + sharedSecrets []Hash256, routingInfoLen int) ([]byte, error) { + numHops := path.TrueRouteLength() // We have to generate a filler that matches all but the last hop (the @@ -332,7 +376,7 @@ func generateHeaderPadding(key string, path *PaymentPath, sharedSecrets []Hash25 for i := 0; i < numHops-1; i++ { // Sum up how many frames were used by prior hops. - fillerStart := routingInfoSize + fillerStart := routingInfoLen for _, p := range path[:i] { fillerStart -= p.HopPayload.NumBytes() } @@ -340,15 +384,21 @@ func generateHeaderPadding(key string, path *PaymentPath, sharedSecrets []Hash25 // The filler is the part dangling off of the end of the // routingInfo, so offset it from there, and use the current // hop's frame count as its size. - fillerEnd := routingInfoSize + path[i].HopPayload.NumBytes() + fillerEnd := routingInfoLen + path[i].HopPayload.NumBytes() streamKey := generateKey(key, &sharedSecrets[i]) - streamBytes := generateCipherStream(streamKey, numStreamBytes) + + streamBytesLen, err := numStreamBytes(routingInfoLen) + if err != nil { + return nil, err + } + + streamBytes := generateCipherStream(streamKey, streamBytesLen) xor(filler, filler, streamBytes[fillerStart:fillerEnd]) } - return filler + return filler, nil } // Encode serializes the raw bytes of the onion packet into the passed @@ -365,7 +415,8 @@ func (f *OnionPacket) Encode(w io.Writer) error { return err } - if _, err := w.Write(f.RoutingInfo[:]); err != nil { + _, err := w.Write(f.RoutingInfo) + if err != nil { return err } @@ -404,14 +455,24 @@ func (f *OnionPacket) Decode(r io.Reader) error { return ErrInvalidOnionKey } - if _, err := io.ReadFull(r, f.RoutingInfo[:]); err != nil { + // To figure out the length of the routing info, we'll read all the + // remaining bytes from the reader. + routingInfoAndMAC, err := io.ReadAll(r) + if err != nil { return err } - if _, err := io.ReadFull(r, f.HeaderMAC[:]); err != nil { - return err + // The packet must have at least enough bytes for the HMAC. + if len(routingInfoAndMAC) < HMACSize { + return ErrMissingHMAC } + // With the remainder of the packet read, we can now properly slice the + // routing information and the MAC. + routingInfoLen := len(routingInfoAndMAC) - HMACSize + f.RoutingInfo = routingInfoAndMAC[:routingInfoLen] + copy(f.HeaderMAC[:], routingInfoAndMAC[routingInfoLen:]) + return nil } @@ -644,11 +705,12 @@ func unwrapPacket(onionPkt *OnionPacket, sharedSecret *Hash256, dhKey := onionPkt.EphemeralKey routeInfo := onionPkt.RoutingInfo headerMac := onionPkt.HeaderMAC + routingInfoLen := len(routeInfo) // Using the derived shared secret, ensure the integrity of the routing // information by checking the attached MAC without leaking timing // information. - message := append(routeInfo[:], assocData...) + message := slices.Concat(routeInfo, assocData) calculatedMac := calcMac(generateKey("mu", sharedSecret), message) if !hmac.Equal(headerMac[:], calculatedMac[:]) { return nil, nil, ErrInvalidOnionHMAC @@ -657,13 +719,17 @@ func unwrapPacket(onionPkt *OnionPacket, sharedSecret *Hash256, // Attach the padding zeroes in order to properly strip an encryption // layer off the routing info revealing the routing information for the // next hop. + streamBytesLen, err := numStreamBytes(routingInfoLen) + if err != nil { + return nil, nil, err + } streamBytes := generateCipherStream( - generateKey("rho", sharedSecret), numStreamBytes, + generateKey("rho", sharedSecret), streamBytesLen, ) - zeroBytes := bytes.Repeat([]byte{0}, MaxPayloadSize) - headerWithPadding := append(routeInfo[:], zeroBytes...) + zeroBytes := bytes.Repeat([]byte{0}, routingInfoLen) + headerWithPadding := slices.Concat(routeInfo, zeroBytes) - hopInfo := make([]byte, numStreamBytes) + hopInfo := make([]byte, streamBytesLen) xor(hopInfo, headerWithPadding, streamBytes) // Randomize the DH group element for the next hop using the @@ -683,8 +749,8 @@ func unwrapPacket(onionPkt *OnionPacket, sharedSecret *Hash256, // With the necessary items extracted, we'll copy of the onion packet // for the next node, snipping off our per-hop data. - var nextMixHeader [routingInfoSize]byte - copy(nextMixHeader[:], hopInfo[hopPayload.NumBytes():]) + var nextMixHeader = make([]byte, routingInfoLen) + copy(nextMixHeader, hopInfo[hopPayload.NumBytes():]) innerPkt := &OnionPacket{ Version: onionPkt.Version, EphemeralKey: nextDHKey, @@ -847,3 +913,16 @@ func (t *Tx) Commit() ([]ProcessedPacket, *ReplaySet, error) { return t.packets, rs, err } + +// numStreamBytes is the number of bytes that needs to be produced by our CSPRNG +// for the key stream implementing our stream cipher to encrypt/decrypt the mix +// header. The routingInfoSize bytes at the end are used to encrypt/decrypt the +// fillers when processing the packet of generating the HMACs when creating the +// packet. +func numStreamBytes(routingInfoSize int) (uint, error) { + if routingInfoSize < 0 { + return 0, ErrNegativeRoutingInfoSize + } + + return uint(routingInfoSize) * streamBytesMultiplier, nil +} diff --git a/sphinx_test.go b/sphinx_test.go index ed4ea1c..588e732 100644 --- a/sphinx_test.go +++ b/sphinx_test.go @@ -2,6 +2,7 @@ package sphinx import ( "bytes" + "crypto/rand" "encoding/hex" "encoding/json" "errors" @@ -44,10 +45,17 @@ var ( var ( // ErrInsufficientHops is returned when there are not enough hops to // create a route. - ErrInsufficientHops = errors.New("at least 2 hops are required to " + + ErrInsufficientHops = errors.New("at least 1 hop is required to " + "create an onion message route") ) +// Session keys used in onion message tests. +var ( + sessionKeyA = bytes.Repeat([]byte{'A'}, 32) + sessionKeyB = bytes.Repeat([]byte{'B'}, 32) + sessionKeyC = bytes.Repeat([]byte{'C'}, 32) +) + // encodeTLVRecord encodes a TLV record with the given type and value. func encodeTLVRecord(recordType uint64, value []byte) []byte { var buf bytes.Buffer @@ -157,6 +165,194 @@ func newTestRoute(numHops int) ([]*Router, *PaymentPath, *[]HopData, *OnionPacke return nodes, &route, &hopsData, fwdMsg, nil } +// newOnionMessageRoute creates a new onion message route with the specified +// number of hops. It concatenates two blinded paths right in the middle of the +// route, hence it needs at least 2 hops. +func newOnionMessageRoute(numHops int) (*OnionPacket, *PaymentPath, []*Router, + error) { + + if numHops < 1 { + return nil, nil, nil, ErrInsufficientHops + } + + // Create routers for each hop. + nodes := make([]*Router, numHops) + for i := range nodes { + privKey, _ := btcec.NewPrivateKey() + nodes[i] = NewRouter( + &PrivKeyECDH{PrivKey: privKey}, NewMemoryReplayLog(), + ) + } + + // Split the nodes into two parts for creating two blinded paths. If + // numHops equals one, this will create an empty first path. + mid := numHops / 2 + firstPathNodes := nodes[:mid] + secondPathNodes := nodes[mid:] + + // Create the sessions keys for the two blinded paths. + firstSessionKey, _ := btcec.PrivKeyFromBytes(sessionKeyA) + secondSessionKey, _ := btcec.PrivKeyFromBytes(sessionKeyB) + + // Create the first blinded path, adding a next_path_key_override TLV + // at the last node. + blindedPath, err := createBlindedPath( + firstPathNodes, secondPathNodes, firstSessionKey, + secondSessionKey, + ) + if err != nil { + return nil, nil, nil, err + } + + // Create the route from the blinded path, always adding the + // hop.CipherText as a TLV field type 4. + var route PaymentPath + for i, hop := range blindedPath.BlindedHops { + var b bytes.Buffer + + if i == len(blindedPath.BlindedHops)-1 { + hello := []byte("hello") + // Encode TLV record for type 4 (cipher text) + b.Write(encodeTLVRecord(4, hop.CipherText)) + // Encode TLV record for type 65 (hello message) + b.Write(encodeTLVRecord(65, hello)) + } else { + // Encode TLV record for type 4 (cipher text) + b.Write(encodeTLVRecord(4, hop.CipherText)) + } + + route[i] = OnionHop{ + NodePub: *hop.BlindedNodePub, + HopPayload: HopPayload{ + Type: PayloadTLV, + Payload: b.Bytes(), + }, + } + } + + // According to BOLT04 we SHOULD set onion_message_packet len to 1366 or + // 32834. This means a payload size of 1300 (MaxRoutingPayloadSize) or + // 32768 (MaxOnionMessagePayloadSize) bytes. By checking the total + // payload size of the route we can determine which one to use. + payloadSize := MaxRoutingPayloadSize + if route.TotalPayloadSize() > MaxRoutingPayloadSize { + payloadSize = MaxOnionMessagePayloadSize + } + + // Generate the onion packet. + sessionKey, _ := btcec.PrivKeyFromBytes(sessionKeyC) + + onionPacket, err := NewOnionPacket( + &route, sessionKey, nil, DeterministicPacketFiller, + WithMaxPayloadSize(payloadSize), + ) + if err != nil { + return nil, nil, nil, err + } + + return onionPacket, &route, nodes, nil +} + +func createBlindedPath(firstPathNodes []*Router, secondPathNodes []*Router, + firstSessionKey *btcec.PrivateKey, + secondSessionKey *btcec.PrivateKey) (*BlindedPath, error) { + + firstPathInfos := make([]*HopInfo, len(firstPathNodes)) + for i, node := range firstPathNodes { + nextNodeID := node.onionKey.PubKey().SerializeCompressed() + + var b bytes.Buffer + if i == len(firstPathNodes)-1 { + secondsSessPub := secondSessionKey.PubKey() + pathKeyOverride := secondsSessPub.SerializeCompressed() + // Encode TLV record for type 4 (next node ID) + b.Write(encodeTLVRecord(4, nextNodeID)) + // Encode TLV record for type 8 (path key override) + b.Write(encodeTLVRecord(8, pathKeyOverride)) + } else { + // Encode TLV record for type 4 (next node ID) + b.Write(encodeTLVRecord(4, nextNodeID)) + } + + firstPathInfos[i] = &HopInfo{ + NodePub: node.onionKey.PubKey(), + PlainText: b.Bytes(), + } + } + + // The first blinded path may be empty if the sender has a direct + // channel with the introduction point. + var firstBlindedPath *BlindedPathInfo + if len(firstPathNodes) > 0 { + var err error + + firstBlindedPath, err = BuildBlindedPath( + firstSessionKey, firstPathInfos, + ) + if err != nil { + return nil, err + } + } + + // Create the second blinded path, omitting the next_node_id TLV for the + // last node. + secondPathInfos := make([]*HopInfo, len(secondPathNodes)) + for i, node := range secondPathNodes { + nextNodeID := node.onionKey.PubKey().SerializeCompressed() + + var b bytes.Buffer + if i == len(secondPathNodes)-1 { + pathID := make([]byte, 20) + + _, err := rand.Read(pathID) + if err != nil { + return nil, err + } + // Encode TLV record for type 6 (path ID) + b.Write(encodeTLVRecord(6, pathID)) + } else { + // Encode TLV record for type 4 (next node ID) + b.Write(encodeTLVRecord(4, nextNodeID)) + } + + secondPathInfos[i] = &HopInfo{ + NodePub: node.onionKey.PubKey(), + PlainText: b.Bytes(), + } + } + + secondBlindedPath, err := BuildBlindedPath( + secondSessionKey, secondPathInfos, + ) + if err != nil { + return nil, err + } + + var blindedPath *BlindedPath + if len(firstPathNodes) == 0 { + // If the sender has a direct channel to the introduction point + // of the blinded path the receiver gave us, then the sender + // doesn't need to create its own blinded path. We can just use + // the receiver's blinded path as is. + blindedPath = secondBlindedPath.Path + } else { + // Otherwise, we use the introduction point of the blinded path + // created by the sender and concatenate the hops of both paths. + introductionPoint := firstBlindedPath.Path.IntroductionPoint + blindingPoint := firstBlindedPath.Path.BlindingPoint + blindedPath = &BlindedPath{ + IntroductionPoint: introductionPoint, + BlindingPoint: blindingPoint, + BlindedHops: append( + firstBlindedPath.Path.BlindedHops, + secondBlindedPath.Path.BlindedHops..., + ), + } + } + + return blindedPath, nil +} + func TestBolt4Packet(t *testing.T) { var ( route PaymentPath @@ -215,14 +411,14 @@ func TestBolt4Packet(t *testing.T) { } } -// TestTLVPayloadMessagePacket tests the creation and encoding of an onion -// message packet that uses a TLV payload for each hop in the route. This test -// uses the test vectors defined in the BOLT 4 specification. The test reads a -// JSON file containing a predefined route, session key, and the expected final -// onion packet. It then constructs the route hop-by-hop, manually creating the -// TLV payload for each, before creating a new onion packet with NewOnionPacket. -// The test concludes by asserting that the newly encoded packet is identical to -// the one specified in the test vector. +// TestTLVPayloadMessagePacket tests the creation and encoding of an +// onion_message_packet that uses a TLV payload for each hop in the route. This +// test uses the test vectors defined in the BOLT 4 specification. The test +// reads a JSON file containing a predefined route, session key, and the +// expected final onion_message_packet. It then constructs the route hop-by-hop, +// manually creating the TLV payload for each, before creating a new onion +// packet with NewOnionPacket. The test concludes by asserting that the newly +// encoded packet is identical to the one specified in the test vector. func TestTLVPayloadMessagePacket(t *testing.T) { t.Parallel() @@ -438,7 +634,7 @@ func TestSphinxSingleHop(t *testing.T) { // The destination node should detect that the packet is destined for // itself. if processedPacket.Action != ExitNode { - t.Fatalf("processed action is correct, is %v should be %v", + t.Fatalf("processed action is incorrect, is %v should be %v", processedPacket.Action, ExitNode) } } @@ -722,6 +918,107 @@ func mustNewLegacyHopPayload(hopData *HopData) HopPayload { return payload } +// TestPaymentPathTotalPayloadSizeExceeds1300 tests that a PaymentPath can have +// a TotalPayloadSize greater than 1300 bytes. +func TestPaymentPathTotalPayloadSizeExceeds1300(t *testing.T) { + t.Parallel() + + onionPacket, route, _, err := newOnionMessageRoute(15) + require.NoError(t, err, "newOnionMessageRoute should not return an "+ + "error") + + totalSize := route.TotalPayloadSize() + require.Greater(t, totalSize, 1300, "TotalPayloadSize should be "+ + "greater than 1300") + + require.Len(t, onionPacket.RoutingInfo, MaxOnionMessagePayloadSize, + "RoutingInfo length should be equal to "+ + "MaxOnionMessagePayloadSize") +} + +// TestSingleHopOnionMessage test that we can create and encode a single-hop +// onion message packet. +func TestSingleHopOnionMessage(t *testing.T) { + t.Parallel() + + packet, route, nodes, err := newOnionMessageRoute(1) + require.NoError(t, err, "newOnionMessageRoute should not return an "+ + "error") + + require.Equal(t, 1, route.TrueRouteLength()) + + // Start the ReplayLog and defer shutdown + err = nodes[0].log.Start() + require.NoError(t, err, "unable to start ReplayLog") + + defer func() { + require.NoError(t, nodes[0].log.Stop()) + }() + + // In a single-hop onion message the blinding point is the session key + // used by the receiver to create its blinded path. The sender didn't + // need to create a blinded path since it has a direct channel to the + // receiver/introduction point. Knowing this we use the session key that + // the receiver used. + blindingPoint, _ := btcec.PrivKeyFromBytes(sessionKeyB) + + // Simulating a direct single-hop onion message, send the sphinx packet + // to the destination node, making it process the packet fully. + processedPacket, err := nodes[0].ProcessOnionPacket( + packet, nil, 1, WithBlindingPoint(blindingPoint.PubKey()), + ) + if err != nil { + t.Fatalf("unable to process sphinx packet: %v", err) + } + + // The destination node should detect that the packet is destined for + // itself. + if processedPacket.Action != ExitNode { + t.Fatalf("processed action is incorrect, is %v should be %v", + processedPacket.Action, ExitNode) + } +} + +// TestCustomPayloadSize tests that we can create an onion packet with any size +// of routing info. +func TestCustomPayloadSize(t *testing.T) { + t.Parallel() + + customPayloadSize := int(1234) + + // First, create a privKey that will act as the destination hop in the + // path. + privKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + // Next, create a session key for the onion packet. + sessionKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + // We'll create a simple one-hop path. + path := &PaymentPath{ + { + NodePub: *privKey.PubKey(), + }, + } + + // The hop payload will be extremely simple + payload, err := NewTLVHopPayload([]byte{1, 2, 3}) + require.NoError(t, err) + + path[0].HopPayload = payload + + // Now, create the onion packet. + onionPacket, err := NewOnionPacket( + path, sessionKey, nil, DeterministicPacketFiller, + WithMaxPayloadSize(customPayloadSize), + ) + require.NoError(t, err) + + require.Len(t, onionPacket.RoutingInfo, customPayloadSize, + "RoutingInfo length should be equal to customPayloadSize") +} + // TestSphinxHopVariableSizedPayloads tests that we're able to fully decode an // EOB payload that was targeted at the final hop in a route, and also when // intermediate nodes have EOB data encoded as well. Additionally, we test that @@ -841,7 +1138,7 @@ func TestSphinxHopVariableSizedPayloads(t *testing.T) { Payload: bytes.Repeat([]byte("a"), 500), }, }, - expectedError: ErrMaxRoutingInfoSizeExceeded, + expectedError: ErrPayloadSizeExceeded, }, } @@ -849,7 +1146,7 @@ func TestSphinxHopVariableSizedPayloads(t *testing.T) { nextPkt, routers, err := newEOBRoute( testCase.numNodes, testCase.eobMapping, ) - if testCase.expectedError != err { + if !errors.Is(err, testCase.expectedError) { t.Fatalf("#%v: unable to create eob "+ "route: %v", testCase, err) } From eb6bd5c734a827bf9b62ebe840816ee855c24008 Mon Sep 17 00:00:00 2001 From: Gijs van Dam Date: Wed, 23 Jul 2025 18:09:26 +0200 Subject: [PATCH 8/9] chore: refactor if-then into switch case --- path_test.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/path_test.go b/path_test.go index d97ab34..5fffdee 100644 --- a/path_test.go +++ b/path_test.go @@ -375,9 +375,10 @@ func TestOnionRouteBlinding(t *testing.T) { priv := privKeyFromString(hop.NodePrivKey) - if i == introPointIndex { + switch i { + case introPointIndex: blindingPoint = firstBlinding - } else if i == concatIndex { + case concatIndex: blindingPoint = blindingOverride } From 4fa0d14d7f29f7aef159b36e11f2669cc70c4e1b Mon Sep 17 00:00:00 2001 From: Gijs van Dam Date: Sun, 26 Oct 2025 03:31:31 +0100 Subject: [PATCH 9/9] chore: fix linter errors --- .golangci.yml | 5 +++++ error.go | 17 +++++++---------- path_test.go | 4 +--- sphinx.go | 13 ++++++++----- sphinx_test.go | 7 +------ 5 files changed, 22 insertions(+), 24 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 80450ba..f16c7f5 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -52,6 +52,11 @@ linters: # Checks the number of statements in a function. statements: 50 + tagliatelle: + case: + rules: + json: snake + wsl_v5: # We adopt a more relaxed cuddling rule by enabling # `allow-whole-block`. This allows a variable declaration to be diff --git a/error.go b/error.go index 3a9f312..14cb777 100644 --- a/error.go +++ b/error.go @@ -1,27 +1,24 @@ package sphinx -import ( - "errors" - "fmt" -) +import "errors" var ( // ErrReplayedPacket is an error returned when a packet is rejected // during processing due to being an attempted replay or probing // attempt. - ErrReplayedPacket = fmt.Errorf("sphinx packet replay attempted") + ErrReplayedPacket = errors.New("sphinx packet replay attempted") // ErrInvalidOnionVersion is returned during decoding of the onion // packet, when the received packet has an unknown version byte. - ErrInvalidOnionVersion = fmt.Errorf("invalid onion packet version") + ErrInvalidOnionVersion = errors.New("invalid onion packet version") - // ErrInvalidOnionHMAC is returned during onion parsing process, when received - // mac does not corresponds to the generated one. - ErrInvalidOnionHMAC = fmt.Errorf("invalid mismatched mac") + // ErrInvalidOnionHMAC is returned during onion parsing process, when + // received mac does not corresponds to the generated one. + ErrInvalidOnionHMAC = errors.New("invalid mismatched mac") // ErrInvalidOnionKey is returned during onion parsing process, when // onion key is invalid. - ErrInvalidOnionKey = fmt.Errorf("invalid onion key: pubkey isn't on " + + ErrInvalidOnionKey = errors.New("invalid onion key: pubkey isn't on " + "secp256k1 curve") // ErrLogEntryNotFound is an error returned when a packet lookup in a diff --git a/path_test.go b/path_test.go index 5fffdee..a808b36 100644 --- a/path_test.go +++ b/path_test.go @@ -537,7 +537,6 @@ type decryptOnionMessageData struct { } type decryptHops struct { - //nolint:tagliatelle Onion string `json:"onion"` NodePrivKey string `json:"node_privkey"` NextBlinding string `json:"next_blinding"` @@ -592,8 +591,7 @@ type generateOnionMessageData struct { } type unblindedHop struct { - NodePrivKey string `json:"node_privkey"` - //nolint:tagliatelle + NodePrivKey string `json:"node_privkey"` EphemeralPubKey string `json:"ephemeral_pubkey"` DecryptedData string `json:"decrypted_data"` NextEphemeralPubKey string `json:"next_ephemeral_pubkey"` diff --git a/sphinx.go b/sphinx.go index 9690154..d56959f 100644 --- a/sphinx.go +++ b/sphinx.go @@ -228,7 +228,7 @@ func NewOnionPacket(paymentPath *PaymentPath, sessionKey *btcec.PrivateKey, // exit early. numHops := paymentPath.TrueRouteLength() if numHops == 0 { - return nil, fmt.Errorf("route of length zero passed in") + return nil, ErrZeroHops } totalPayloadSize := paymentPath.TotalPayloadSize() @@ -407,20 +407,23 @@ func generateHeaderPadding(key string, path *PaymentPath, func (f *OnionPacket) Encode(w io.Writer) error { ephemeral := f.EphemeralKey.SerializeCompressed() - if _, err := w.Write([]byte{f.Version}); err != nil { + _, err := w.Write([]byte{f.Version}) + if err != nil { return err } - if _, err := w.Write(ephemeral); err != nil { + _, err = w.Write(ephemeral) + if err != nil { return err } - _, err := w.Write(f.RoutingInfo) + _, err = w.Write(f.RoutingInfo) if err != nil { return err } - if _, err := w.Write(f.HeaderMAC[:]); err != nil { + _, err = w.Write(f.HeaderMAC[:]) + if err != nil { return err } diff --git a/sphinx_test.go b/sphinx_test.go index 588e732..dd550c9 100644 --- a/sphinx_test.go +++ b/sphinx_test.go @@ -105,12 +105,7 @@ func newTestRoute(numHops int) ([]*Router, *PaymentPath, *[]HopData, *OnionPacke // Create numHops random sphinx nodes. for i := 0; i < len(nodes); i++ { - privKey, err := btcec.NewPrivateKey() - if err != nil { - return nil, nil, nil, nil, fmt.Errorf("Unable to "+ - "generate random key for sphinx node: %v", err) - } - + privKey, _ := btcec.NewPrivateKey() nodes[i] = NewRouter( &PrivKeyECDH{PrivKey: privKey}, NewMemoryReplayLog(), )