diff --git a/bench_test.go b/bench_test.go index ef9e8ba..57f0c08 100644 --- a/bench_test.go +++ b/bench_test.go @@ -33,7 +33,7 @@ func BenchmarkPathPacketConstruction(b *testing.B) { } copy(hopData.NextAddress[:], bytes.Repeat([]byte{byte(i)}, 8)) - hopPayload, err := NewHopPayload(&hopData, nil) + hopPayload, err := NewLegacyHopPayload(&hopData) if err != nil { b.Fatalf("unable to create new hop payload: %v", err) } @@ -77,7 +77,9 @@ func BenchmarkProcessPacket(b *testing.B) { pkt *ProcessedPacket ) for i := 0; i < b.N; i++ { - pkt, err = path[0].ProcessOnionPacket(sphinxPacket, nil, uint32(i)) + pkt, err = path[0].ProcessOnionPacket( + sphinxPacket, nil, uint32(i), + ) if err != nil { b.Fatalf("unable to process packet %d: %v", i, err) } diff --git a/cmd/example-data/hop-data.json b/cmd/example-data/hop-data.json new file mode 100644 index 0000000..72c03cf --- /dev/null +++ b/cmd/example-data/hop-data.json @@ -0,0 +1,26 @@ +{ + "session_key": "4141414141414141414141414141414141414141414141414141414141414141", + "associated_data": "4242424242424242424242424242424242424242424242424242424242424242", + "hops": [ + { + "pubkey": "02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619", + "payload": "1202023a98040205dc06080000000000000001" + }, + { + "pubkey": "0324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c", + "payload": "52020236b00402057806080000000000000002fd02013c0102030405060708090a0b0c0d0e0f0102030405060708090a0b0c0d0e0f0102030405060708090a0b0c0d0e0f0102030405060708090a0b0c0d0e0f" + }, + { + "pubkey": "027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007", + "payload": "12020230d4040204e206080000000000000003" + }, + { + "pubkey": "032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991", + "payload": "1202022710040203e806080000000000000004" + }, + { + "pubkey": "02edabbd16b41c8371b92ef2f04c1185b4f03b6dcd52ba9b78d9d7c89c8f221145", + "payload": "fd011002022710040203e8082224a33562c54507a9334e79f0dc4f17d407e6d7c61f0e2f3d0d38599502f617042710fd012de02a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a" + } + ] +} diff --git a/cmd/example-data/onion-blinded.json b/cmd/example-data/onion-blinded.json new file mode 100644 index 0000000..9db81a6 --- /dev/null +++ b/cmd/example-data/onion-blinded.json @@ -0,0 +1,6 @@ +{ + "session_key": "4343434343434343434343434343434343434343434343434343434343434343", + "associated_data": "4242424242424242424242424242424242424242424242424242424242424242", + "blinding_point": "034e09f450a80c3d252b258aba0a61215bf60dda3b0dc78ffb0736ea1259dfd8a0", + "onion": "000288b48876fb0dc0d7375233ccaf2910dc0dc81ba52e5a7906f00d75e0d58dbd4bb7c2714870529410735f0951e72cbe981e2e167c0d8f3de33a36e39e78465aea2acad1e23c78b6fd342d63e37d214c912b4a0be344618f779138edc1b42a5ca3218ca2fea4be427f6cd0d387160db2bf6c2ba8e82941c8cf3626bd6bed7187f633012ef49df38f6b12963cb639e9eed1b9d269dcebcbd0b25287aa536ec85e7320b02e193122199a745ccbaaebd37f5d4b71f52f9b50feeb793eeef56924a046bc5e7003f6253e0284a8d3fe2e42c3564050f1e753cd32cc258ac0ffa6e05eecad5ba1286f78252e60dd884a65405ab673a85ba52adfa65c1086d4bb37ba2e0848adb2b04379775ad798492b14e8997f30ffa9cf5d432bdf5b246fce008fd876399beed827db58195f4f6192f6ff4ec63cb17fdcb497cb7aec26846a71dd8dca02fc3bb14dd7231a4d62a981bec54b71eb20331096dfa214a0ff4489ee96db663826ae8c850e9f06baa52a47b8eb576363f97e742aab2dc616acc6e74588e1d2ac16694febc90abaf5b1c684163c0e615a68d32633f01934adc8c6bf91fa3fd7aad033b7596d60402494e45e2c1632c40f7bfbd88a81a896a1d28ed6338c83e1eeaa467945d59998eb456c95f94bf1892e8f326ec2d5e0196b7073f106febc6ab8ca5bcc23f77ffc819bc1b5debce418ccc7d8391bbf33bceee6110beba170121bd99f54c956e64970bdab31227b03ee0ea3f01fbd9bd74015f6f82d04fab072e8f85f4370d09f41ee3e48eb959767bd989abb4eea42c4daa0437a7f747d7f9b70eb87b9f9b0b6f283b8205912601a432999b8869fd9fe5bad3572edac24da7184f9298f21ff60923db277264d29c846dd2f228f6fc53b6b60364237de64773f803f174ed10229c374f603ccc5fd3a62cb413ffe6f5630dc646bb33f231b2350537ec39e5d3f2fe1a1cb019ed0b18ad14019cad27afcca8ad70387ca110394c0432774f1aa1fa404b2e086c84a55388d3bd102501c78ef925cce89d76fa04c3f20f2d1f0ce507ac8b37b7913e3949ba12bbc5a4f6bac37c2415622d365bc8b83709a28e3d46f3850c89a3ff4d027fef6e3e4ce5c6c85f663c7eaec3c9730106fb82f53249a905533cfabee812aae51965b24b42f7ab471967bc8e73354e69141ee26a1f03684d5fb9c256a34de8257210e0390dd3962db521ae0a3bdab28300610ab2a634b699e5f092da5a061609ef6414bd805c8171f54ad6f285fb64ce0becca0b61188badcf8ef21190dad629e3fb3e89f55ebba829919540ebf5f8ae4283836d3c9133c1ca3365f6b9394916730411650686e0c2ab9c53b6cda9efdd5cfcb53ba9b6962bb6aa49d0a83a87460b60a9c7d2643ee99afe652883795f14014ec5df61b1e30c041c1fa6487f3c82f1ded5f83ffbef5017e197b7fb77be3b36e284a15e57d45bf9316dcaf97eb78ee4642b731ba05c5063bce1333fab4af6da97c80a96ee599b4df823efbedc250c0abba9783da7ddf2414b2a4774ff2880a7dc6791103e18b8631e39743cf9e87aed71700daa5dc72fdae520324741f92ea3d510ff555dea5e45f15cda87272d4559a12d4777680acb06993840e3c748da82c16cae556015fb2acd0335da11a3388575394048ab71199793ab706abc9d68add2075d79a5cc0f779845ee8b98951be61fd293d6c15b9d4653935bf17cf50bd31f8b79e60dba0e7fd6864754fd94262485a4f65e7eb3e1922f51b1a4dd2b4fd2c20d94d1213fbe90bd603dfc7e15176382e3ce0f43f980d44d23bf3c57f54a15f42c171a8f2511e28ac178c6f01396e50397a57ffb09c5e6c315bd3ae7983577c1a0386c6d5d9a2223438e321b0fedfdee58fa452d57dc11a256834bb49ac9deeec88e4bf563c7340f44a240caec941c7e50f09cf" +} diff --git a/cmd/example-data/onion-blinded2.json b/cmd/example-data/onion-blinded2.json new file mode 100644 index 0000000..29e1025 --- /dev/null +++ b/cmd/example-data/onion-blinded2.json @@ -0,0 +1,6 @@ +{ + "session_key": "4444444444444444444444444444444444444444444444444444444444444444", + "associated_data": "4242424242424242424242424242424242424242424242424242424242424242", + "blinding_point": "031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f", + "onion": "0003f25471c0f2ff549a7fd7859100306bb6c294c209f34c77f782897f184b967c498efc246bdb8e060a6d1cf8dd0d4d732e33311fb96c9e9f1274005fa3d08b41704a1b7224c6300a7caead7baa0a8263eba2e0de6956ee8e4a1958264f47e4cf20d194eb576f5bd249ee4fece563f80fd76dc3eaca8f956188406d83195752b5c90c4b2a5e7ac3a8d5c62b17b551aff48ef6842a7e9326832c9a4a2fd415011150a9e71beb901fd9747bac8add1c694b612730dc86b5b19a0bbbc675947a953316e3303d7b30c182f94def9206671edac9a3ec3e52d28fc28247a1c73ab751bf61c82c3950f617e758f79bd0ba294defb20466eaf1e801462046baad3aec3e5b8868a7b037f23d73a47a7e74c77107334f37388cff863e452820c61d89728fa75c84bc7cdfc06dcdd1911f5f803353926d073efd65251380e174913aae03318ea5b6f0ec83998c55ab99bef62803ea2da9f6d1ea892b90efc4f8ffb685a5201a781da2e6ac5923645638c9709ae32171a00c0cd3d8c7eedfb06b4eedc7d3e566987e2e3805a038f21d78ded5d6c7137a5e8e592f3180ee4d5f4e1289176f67fc38690d0958bc82e240b72b10577f340f1e14b8633f0b6d9729ff4618be2a972400a015a871ba33be70335f652a8d70f2bd32421d6ac2af781d667dad787d6aef4505a15d046579e46eebe757444cffca6d0610f0dd36a7ce57af969bd0c3f7006298ef406a25f689daf58f875d44d2423ebf195b503f11c37c506ea6abe50a463f7bb5e9b964604d832724de768513f6b38bf71715e1feea8a6e86797788d487146891564919da1372016ed8f08c7fcbff66a4a65a3d0fcd8e3daac6eba41f5d65ef2d8075364a9e78b3a273549f6eac4abb72e912e237990367e0d43e89945f8ac3907be5a6c662139485a50cb5ce3f0ba08586c39f6c515368ec3f91b72295f1b7a73a9df322ae9a45d363d6c616be3300083764cbdee31221f25a318f095feacb09f957c96db30fccca47a0215b576c3ed925a0bad05d6400abe318c11f36628c387a4ee38832182cd44b3cd48e5422c1f1e3b57218dfe72c611f5415127720e60f6e2400607e61841b76de1704bcbeb0daf1377ccb2253916de2b6d490bb71ba0a44fea2e94f2423d723934557d5905e01b2b80232a884e258d46dc92ea11e0818d0ece5b914f02049866e151801ab8c9aea155479b354dc91151fb9ba43277458f9760dd859faaa139e3b9ab36a1dbc36a93ef2c90598b20cb30ef3c4f23a2d6178b4d1da668fb328a25d84d30a132d9f2a6a988cbe2e5c2be01cb6db4b4725a50d6cdacf5fb083e7d650a25bec1407fbc047d26076c7596429a29606ad527e97ef0824ad6c1b05831a3e5b71c63a528918a3301cdd4061fc1fcce3da601961f2602a2b002ac8404125c2d52666263858a923e197efcda873c32d86897352e4f2264ad6a1b48acc0fe78ff55cb442cb2bb5fa2880810e1d00aa0247057fb80b7ed36cf9647af41b44ee4a63ee2d6f652526404572520a7d2d9dcde4e62df0c3be89f8471550594cdd16a51a9cacc58729c092c68506162fe65edc2314055d389f724ced189d826a546b5c4d08a43d977b3cf033de5760b71a7cc38ee5851592031aafb467a89b3b6c7ed67b15d44c48d6baedce3e95e08ec7c55038f3eba90ccb900895734f0fb7efe54961ce493369cc56416898a9bed7c2482871c15a7f1eb5ed17c33657fc31333539c2dfb59461af09e7049228113b5c9feea5a6e9959c18c51b19c90995afb9c76f2c0c820964cd7989c993a73925818a656c6a18dcd1a1e3782b2eae06dd5a41250ec2d1c203626ab9920c1673339eff04b1eb0cab85ef5909f571f9b83cdf21697c9f5cfa1c76e7bca955510e2126b3bb989a4ac21cf948f965e48bc363d2997437797b4f770e8b65" +} diff --git a/cmd/example-data/onion-blinded3.json b/cmd/example-data/onion-blinded3.json new file mode 100644 index 0000000..5693440 --- /dev/null +++ b/cmd/example-data/onion-blinded3.json @@ -0,0 +1,6 @@ +{ + "session_key": "4545454545454545454545454545454545454545454545454545454545454545", + "associated_data": "4242424242424242424242424242424242424242424242424242424242424242", + "blinding_point": "03e09038ee76e50f444b19abf0a555e8697e035f62937168b80adf0931b31ce52a", + "onion": "0002ef43c4dfe9aee14248c445406ac7980474ce106c504d9025f57963739130adfd06eb26201baee8866af2d1b7a7ab26595349dad002af0590193aaa8f400ab394f5994ec831aeeecb64421c566e3556cbdd7e7e50deb1fc49fd5e007308ab6494415514abff978899623f9b6065ca1e243bb78170118e8b8c8b53b750b59cc1ec017d167adbb3aabab7c2d84fbf94f5d827239f4c2b9d2c3cfe68fe5641f25e386202a4b6edff2a71e700229df7230c8ca31bd5588f04799e9640c9c20a47cba713f3cc5ad3202e14bb520880f2a8409d8e7835cae21b48a651c2d47fe6af785889ab98f1416f6e4ad67a66ae681e9a8828bad3f9b6890221c4a7ec80531d6b63eb30843f613ce644795bc8bcee60e8f7b36f3fd04de762f103c52efaf36a2f3bbbaac482d6271dc4180c10bcc076c04d06ea7fd8fb6a647e0e10523b05da2d89e4139fb55c2315cd01bdcbd57587fef8442d7ff5620630fd2d2e79739d90be811bf2cba60415d6cba2cea14ba1859f3122cd905c4e12e3e2a1ab6fab54b2ec40e434626e2d3c3195c02c82a8bd64d226c2328ac72ca12197d9908eaf54333717448ce6ed73adc0ac05e2ee1d735131d87918beb8995993dc8f63fe10f2c8eba2be7ab8bb44d9f78f59ef3e4c180bd75e4eef2381450c6f0480d543997305f1d07815993b5aca8d88d474966d9abec93bb069a16aa2da75b87f94576e01d08a17d3e0e3d0370f010733a7d7affb12cdf94c259a62607fce71003535c4727305de5ff7bba3840922844b3a45f62c29715fccf440517ef121450f6962396fba9b07036d085582405dcae6ee95964b66bc7c85b8d02d90091500db3cebf6de584f86b7b55335a8c9aa26381b00747f055cc458a2cadfccf9c29702bf941447beaca6583cca09492a57d4b03b2ca00dbaf41dfd6a9b249381626a7debe475735a7e39e77a363eccf14669046f656cc09ad448da8d8b545e6a604f46dc481786d09a94c63cf23f49ba367d2929466364dbce2a8ffce3dadf8f4cef8a56e1fefa1a3304a953fe83018e57d8a95694b02d994fea2630a9a3d5f1e2f6d6142d503ec4152871f7122d7e566a03261f554639e7a759e0e73846f71d5cace37d91336fc9ca9396bf64ca2cf45fa2db779b3b5c63b04f1c0c1fb79fdfcf5a82b0202df934ae1720a7ce1e047cbec3f82737b50168c974f4623cacce87e3f5bd5232caca7956d28ffedcf11ac5998662c5f6b13c6126584ca2e894d3fcbad4d130bbe22e88a135e0020cdd43853e0b3af3800e9544854d211e873cf68ab683578d501d69ec5dc7fce42ac436d58243880c1b88227b0681c6c9dd8a8ad0793202b15ab63b787b748e258da3e68d0e649fc4ac081a71de8adbc891c113d5f722686b6ac4ed9e3cc247bc4a4643416f480627e9de20f7307f434a499f5c6951c2e8b3ff51d455bf65ceb5ee3dee47b968ac2642e13d8a68f903b73627c2e75788fecca5836371a908eea4f1ea44db2315bc185f77e478efeaaa4da2da13fe7aeaa79ed1d04876a8b2b7b333c5de8c4c9a50274c2eb7b9bd2a3630c57173174781fc9785235f830cefa1c82080eaffdef257f18eedc9ddfd25a696a11a3dc56cd836be72f5f4a2cbb6316d5d3b1ad91a7ec7d877f28d2c29a5525b0b24362699281b0e3b48f38caf1085045fe9089f9e6fb29e4b47aa4cecf68c9bf72073469bd9beeea5e88bfe554cb6a81231149ba7fe7784c154fd8b0f9179ecdf1e9fd5c2939ec1ab16df9cbe9359101ebce933d4f65d3f66f87afaecfe9c046b52f4878b6c430329df7bd879fba8864fcbd9b782bf545734699b9b5a66b466dcedc0c9368803b5b0f1232950cef398ad3e057a5db964bd3e5c8a5717b30b41601a4f11ad63afe404cb6f1e8ea5fd7a8e085b65ca5136146febf4d47928dcc9a9e0" +} diff --git a/cmd/example-data/onion.json b/cmd/example-data/onion.json new file mode 100644 index 0000000..e586c7a --- /dev/null +++ b/cmd/example-data/onion.json @@ -0,0 +1,5 @@ +{ + "session_key": "4141414141414141414141414141414141414141414141414141414141414141", + "associated_data": "4242424242424242424242424242424242424242424242424242424242424242", + "onion": "0002eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619f6e34152f83f6bc03769e265421e9615471ab9e10bace8e7b7eb4f846df42abb9a31b9d8a0b0b009cbf93fd14da6a1a6cb448b57cf9783025943e97407e52d8783f7b8854ad661941ceb446b5fe5814c7353354282f4cc3c47a3c8a9b75e4f436fbd25df99e608c4e53f76d752f4bd077dc62660f59edcdb55949856ea1ae07ccece645e2d2f995d95cecc6bf796d1208573ae0985bba253883e9cfdc62be6865829435c732e9c2d793c8c036d23248a4aa15024ca3bb960425a83b91ccd60c6e68d3c6a5bdd96832f8757fb39b26a6c3065a2f7e98b3f594ab5afd95286dd54658df649c500a3db88456999ecb3a8c8746082b191ced3571b88a1a16972bfc25fa2dbde775bb8e6299356e51a862eedd28c574e202668f134337374908c24f145ca0112deda44d020bdfc6e6d0ca5ee863f5ae79c849b0e34801fc785a3348e658abe57fd07a87525b5061f2afc5f0726754d2f97243fe36aef1e0c9fbcd41157f482d9c3348fae5a055ce2218f50f1a7e804451cd5ed55b36e3f72306b4b62fa1b31682ccb3c90ea24a33465da7c414fa022ce9c83b4e47c112f7dcf377282b148a02298d08d0ab5a560ba883452acfbd120be623e169dd7cc7d4dd1441074b21d33d4522c17c381f26084d811c612a337a47d44c9023867869f09da5b4ffd29c1674e8689e542f45cc864710a97073cd7b18039515425e66109b38a3a9ca6a14bab3e6e8c1dc5d28324f4920745d0a162644193f87011eb0edf5cb977508c943043d93f7dedf81a366ef6467df908decf4e55236171d17c6fe6995a982fefaa13a43be1ba5a9399be638b01864685b94e1e5e340e9444c11dbdf47681839d7134a9e498b5f53cc062682661d47ddde6a86e76eb0db9fdf1142f0bf4c30e512360967a3f3fd4c770b00d15236e52cb807769c770f3246be54563e57b3a243315d39aafff336c6883c23e2caa8405aec3129187f9f810f76e9bcabd5e93af911a88396f77d41b0b3ae2b3ce0884a0a34434577a48cf332bf844eac35493e33e423780a3c70376fc2c9ac8c403c568424e7691f35c48d1f28f37dd7da0b7168534ad819e14f7a09292b077e69e2edae9b73f40b8ad2ac116665465ec142c79594ec5169a890b3de310b26d38e77268a067c7c621b428e986b51a05aa9f2e8a8d88cfc3fd097e96f4aeb08b9de518e42f543c85a8827e55f59efddcd0b0a024fa44ed0617cc582e33555c5e503f2853754f6bc66339d007d8d9292387275e44d04af368baf56eb88ab9dae9ba9b79400aed0457ab277411a534e330ac8c5822f9c7fb50c05c34c43c4e7e38138bad20cb2142ae067b344d243c84fb4f37ce68111292dc40a2f5e99090e8929cf2cb5796d3c67c2b9dc49517a9d47617ed2962bb8aba03e022ef4fb68932ebeefc7341089294caa6e03a64e552897f8e7a60fc7b841a31a7d120fa64109f5f72cf20f96086cea8bdfc5646adecb547b85ca3c414860f674143fa1608e84393e99214f970ba2208ddd32ca8ee0c5d64d8de5ace4e9e47e9f6fb782eb7b6512c9a2740dcb086e82f4068d2677e866b9bc5e744d14e480268b10cdfb8c784c5ff1bdf9f3dd453e76b98694f0910bf81d57752421354823efc5caa5066ea9e63d4834149b1076f0eee0283a5539ec78421730190535e333ea062e0abf244ccd9ae1ce9c0dd856b37d6e3224f6fe4fe01bbd8d568605827bb6a4148b4554593cf173899714495ec3d19e939bd69fa5a259edca0aff4a34e2381d74a62e6277ee869a9f17131be3c7a24e684f129ad7fc746b7f4cbda16d7e36915c8a69177dc8e33390caf159c1f06aa75390e72db37d6cd0d88214e7e929e8ac5f7362be1eb1f2ce6139fd104eccef828" +} \ No newline at end of file diff --git a/cmd/example-data/onion2.json b/cmd/example-data/onion2.json new file mode 100644 index 0000000..6c725cd --- /dev/null +++ b/cmd/example-data/onion2.json @@ -0,0 +1,5 @@ +{ + "session_key": "4242424242424242424242424242424242424242424242424242424242424242", + "associated_data": "4242424242424242424242424242424242424242424242424242424242424242", + "onion": "00028f9438bfbf7feac2e108d677e3a82da596be706cc1cf342b75c7b7e22bf4e6e25017528605ca89bc5823fc2ff825f3a9ae4f431d927817917965078c79bb82b59ab4974eea9bc47204d2caa345bd67a12d5bb56c19eb5d93227e23fa995ad13342c067e3ce6cd55d11ef65ec2bde846a7a0598b99c0dc552157271b069bb323e9d9b0d076850d6288f76a8de29aac690d66fd97f994472dc456cf8a68f6120ef6821d3ed092dd1e4c704c93a4f3fcef0f78a156da42a80f2c1ae19935030ce0507e905a93900079ecb2a1c9e233a92be8e97a557f063a59c95cae29c791ad7550f62f5f1888be1b247e6d55a6de26b136c2896d6758314108a429c82c683968493c807933d6b93e30f09c92e1da6401e3af4d774f775ff84b9536192720637cfdab6aa434c1e6c08baf075a8a3b55c13fb9a36ff59fa9ef8cfcca35fa811c3db716509aaeb5a2979dd6933b5fd5fefec35ebf294c779fbf700bac91fa52cdfaf511265354dd0225b15f72fd761280297f620866fa45f92f61fededb7f024a89643ffdd5f57097daf6202091ba8b77ac7e862f0cdbc5ec03daf0968e11237860004e572e26164ace8d2f7e4f50ce6a5a008934b072749c07358c4016738a994476ea215d3623079692e9f3b68b470e4af4c6985a397ceef8a7ac09490b6f43c440c7f83413c1c468d5f09e3cba23d74c94899a32b382dcb8d3994a16c1e7aa9c5b7135b7b3cfde277f68a0e2c1742a53422e5acaac5218eba290ead01561893c2bd6375f5dd0c24e84f820ab6ccaddae5159b3d2b6372c5b865117f3f003b6ceab16b189a8183e0be79e4bc872825bdf2ac56278ac7f971121c221245c6dd31a3fc04945393ef0880ff6cc22bf5e261e8240591997cd3648da417b0a4842fdadc8f24c08e5af67aa19335343830805919e9d4378900eae26fa5e21d7b9fc54111d2410625004f7baa9afc0d0f1099156db4b43bb87074c5e3beac5a97d4b6d9b84f565186792a18cdf42f44b1f080770d1a4f311f597c8a50d29497414641a1c031a2b561cf347f0a907b0af8d8fdc843ae762880af61e64cef17d7aa13a0f72a0ef0b19489f2a90167dc7795a4dd02c5358fafb05945d86ad638d04d2b8ed8189ec5d299b6f17cedd307c1cf6a2462af84dcf2a404e9a7279bbacab908685a587f118a232bd646dfefd60addbfe36a2b1f7e8ff949052d8ea4c0d236d01bee53962eb718ede8a94ff29cb5e58397d7d32b3e6e63220461539858fa8ed0930463df4277b569c6ca579649c8db11d5a631a0000e722ebd657afc973086bb06dc8a73f6464728b6884e950a68b60c86f88184dd260fda8f0b962c417595632bc547fd46417092a77fdabfedd190882f1e52d48323c4345e9542d723f930a253967923ec8d27e71bbbd0af9aa31544c0b4d6da5eef06be3c3c9049457a371712fff06626f4480e0cd082e933104f12dd23bd01ac8464b21623f542b85b6f5fdbc1daba3318c8dc9803506e666dbe13f7424f0279273a687fe89a1228608c249c5abfee75ac742db27ed68b19312a99651ce947fa98e3945e4f3bdb449c2eec79e9efb746731a0f5d5a1974bec7f60d872e207ef2905ba044ba5118ea2f1b116e1dc1aff9975a3eeff8f2ec5b0d72459bb7af222e586869a87fff3da331520a7d34c4fd58c090bb31e00f1309af259ddee14b94473e4517785d673d79a6e71b3e6ee3fc887ace2ab48c19744b0a3b302fa6613a23c0b18bc35a8dad53a6cbe62f23cf1773414331bd6e99beab684b6ccb839dbf0397e85d106e243d760197a364d586989115ade30d309374fea8435815418038534d12e4ffe88b91406a71d89d5a083e3b8224d86b2be11be32169afb04b9ea997854bb72493084f22105fea7b5181eb70fce6ff3d92bffee12f7f19ba453f6bdfc1ca" +} diff --git a/cmd/example-data/onion3.json b/cmd/example-data/onion3.json new file mode 100644 index 0000000..6e5c89c --- /dev/null +++ b/cmd/example-data/onion3.json @@ -0,0 +1,5 @@ +{ + "session_key": "4343434343434343434343434343434343434343434343434343434343434343", + "associated_data": "4242424242424242424242424242424242424242424242424242424242424242", + "onion": "0003bfd8225241ea71cd0843db7709f4c222f62ff2d4516fd38b39914ab6b83e0da0f13ca6418876711541d1adcd84fae578e80ed4c2aba02acbf1fb3716fd7345e78c0affb24613c2ad822bc7c0e1ea8c9aecfe8305ab1400421c900edf4b4d461df181853cf77799945528b4f40ae3f98ec572ffdffd5d8962bf4d4d8c5780529991c9de9b33176841e7613250616146452e0158206106731b6d1f98cc91bfbd109db6dccf8fb141ee350a99ba0889f011eab62b4b65fa65e2f44d0d5f9adb851e633e3d3654fff80067e594efb67123b5f7343244aff9d457efbad718af0ed16a8788870e3e7b8fde17ef27719e1ddd41e4335c5dcbd14615753b165ac992b9fac7df34ed13bcdbff085d9ec2edec3149307be22ce1b6da70303906139e39840d837ab6d8dd335eb66523637bdeb2b446170cb02994f96d3639b2bc66f1eb46f6c614b70ce6e7f539fb1fe2278035e409fe9f8797bb6fc3604b58c835725bd4e48689e224ff784c6678e54fba9701609b7d3157ea05a7dfb6092b97c362ddca2101f3c1c953a5ef92037d268c0c94ef5f5c082748fd068f2a5b8b8a4c130687591577ddf3d864665a5aded674c33a370e1dda6be247b1ad2885f06c10dc9c1784c869445a7cd0ce5aee125bde1958d11ed3aaa10fdd00d490a58441ed7cc0bfd79d82075929f3b9b93f1f9f61b472c4864787c770a908332b494dac06496d0589d13f61e57b291eb52d70b0f0d431714f842f41b291e469a57989329ea9ac50ae903f51395a51ef554cf4c2cda103f01ca0cde08ed2ad721a25e303bc33d66ebab2d43710378a7e9c5dc211786983f3fc845806f4beaee5833a6b0182396c740285037cce48be4b9552280c3dce3e6f0770f0f2139567441c6d311523632b11348a22cb8c3622acbfb0febc38bebc0d2d260998c13dacb9c90ae1740515f1b21cb6ed53dc0c0aa239f4fc6fde90fc89ec7ab7a113d8ceddfcd3bfe6406a9998b66ce144530b39165e3f05106b85ad99cfcf36fc55abf48cbf388d3331c9961ea20484b72d8d2f831cfb9dbea5c63512c3abbca4148fa238304f698142983777e8cb7806380e37c2c71888bf7bbd8bf1ada837acf18388db21666abfbb6e386dbbcd454a09f0c3ab94ca1b48d9d3b0d7c394f1ce89bb048f9ec3a62c9534262fa989b68719f6b4653c4609614d606030b39d4aca551078b302f1dc0b4fabb1bc9d11f8b3950044cd64befb42075f84337288859bb2e575c8ee0285daf7b2fdb35da2e764e7ea05d30b63e534050fea7111f4712f82e62e9e811c43f1ca0a232e86edc874a0498320bebf60c5dc413f5a7d2a54cd59f0383ca3123ebe51af4e507b8588c23b276813e893ec65aeb795c8b9dc4185acb2cb91fdf39955e55fdeb1865f4192f8fba589fe2cef5c934ab80716d0d3e7e1869d06735a457508b88c795aae03f1b38b4aa3ff36cd20284318048d5fb2ec47ec999a2512ec6dbdfce24276c12ee6bc41eb8e0f53c193a02b1882fe5b4ce61dba6727180bd9ce65a718b8d40471344e8e2a90008d2ebcb65c8cbd6b30c7f8ee57c27460113b29b5c0c1e0bc12a26d2251998ee146235d361579124164b7566d9218dc116f50fab939715074bba73a8cb953e171c187c70022364e140c704a78f93085577f27bf41f9e02f55c155ec186133cfa8b1dd40cd4c773e735a8f15291fccc8c4f55bdcc8ba563004597b5b3de5d54be23671bc9477805ba10d03afb0715782845d2ab45df012f6644207cc5fa4739aa3eaf6bf84e790128aa08aede33bf30c6be2b264b33fac566209e73715a254047517e6aa05e31afb8bd076a3e56fb1c5bbc5dd44f6f095d7f927939ab97b6899cb7bd8bbc5610db5e2556a698c01ffaeda20c8daf398a339af8e2ba28d4b14eb916b64a91a09113431f38193d0" +} diff --git a/cmd/example-data/onion4.json b/cmd/example-data/onion4.json new file mode 100644 index 0000000..b71d833 --- /dev/null +++ b/cmd/example-data/onion4.json @@ -0,0 +1,5 @@ +{ + "session_key": "4444444444444444444444444444444444444444444444444444444444444444", + "associated_data": "4242424242424242424242424242424242424242424242424242424242424242", + "onion": "00031dde6926381289671300239ea8e57ffaf9bebd05b9a5b95beaf07af05cd4359526cc8aa7d0f6392e039a9f2846827fc997c3312fa06cac98e5199c529e328427e7bca6533bfecbbc53af0e076057c647a07d93a056ea323538d4feafbc057a4f654e7732292561b85f349c7ed03c891b888a78050869ae4008c4f0eec16e1d902424c2b3a0a3f7d37684cd5e0c41b841b3449e32af7e3412cddb772701dd91feb687edb7bc26d8b530b32b93465b906001ae610384d3c2a39626e5905340ec5706a7b1f0c8f3cf4221a44649e90d64a9f7cd7bde3fe269782e22a58ee3ab3cc3aae66387003a54ac70a464fffb474b17f591e7d6ca03d8fc6319e2ac03977e6320477ba64272cce10acded883e472d5cb9c151e9d25a6dab669d6b228a96d28dd8a1b745f87f8d4e21851fa26da8c9dd7354a73160e014249ba64bce9a2fa55b67bfd86a05187a15b3989a114aac102c6cf1fa1306b06dcb7f4d3e1c292f09eb52ae2c6295307e43bc373d70a32c1aad7731d624e2a7160e3fdb6e3baa516aa5ce1cbc0227fca581242b0bd6ac2f37213d3d8b68cdcd37fb6a53151467a736f98cebe6dddf2a97a9b6f04155a1f41464d42dec10c9bfbed84c6fbf7859af049f17dcb69cc4e642e243c3fdb9359bf70d0390688cce1d297b584cbd7fc8439ed4366bf1f7a64274a1f0917e0c36f1b3600a87fa4c450bb4fa9d884d0c5bfea6a51b7e1d25157330d72e171a4b7b994dc7c391ae79e63507d86ab07215c9264fab83303c8d0084ab00bc1b8e00a9608eb58278f55b765c5bcb4e4baab79283560859fd63538c80f8b58ccbd2f30623c29b4bc177aab641150496c2ba56bc8d4d6d9630d74692712c9f6a9e4efb3091cff9b7fb0bd285d558e55781183a19c2faa47c32674638750dfab3cdd5998514f8d57c46bd341f1d0ad2b6f08ac93ae1e6acd71c49509a5ced3427adfc4a6b0216bc83171c071a45b2a612ed296d312a7701aa0521319000748d3c1cbd6b3ead745875e08c04b95e7bf3b3cef6008decb530ebef1cb5e8f2e54c8b77b419123b23582899823ddb6c16736be4259c933eeb50d0f456e6c60564fd75490b1485faa2781ffa6fb6aa74599ac3a86ce24ec93d0e281ded02cb851f7730424911a5e59b72c35488e181a874af550915c69219c08386942b1d210bc5ecd7c0f4e9750846e880d36c06e0abccd4431b6aa2da300adcc5413ae323a19d8c3ec390ecc4f382b10c9b2c1d75e580fcae94160ba2fd2ca7fcf7a2ff5d02235319ed5835c28994654e37329d5e9fdbd1d47062e75ef089e6f4a6e822e1badbe620e83346545be79a0ed25be303c60e966c9f630b7ad3b85aa7be800e16edc6a8f9d012969bfad73523f4f3149a3ac8db49931d631dcee7c29d6f43d9882371c79a1eb608c570bc7d49126a0c456951f583cb06fdd45976e6f1ca4bac239202f7b52acbaf62f26ee24778039c855cdb9b0e7649072a142f14cf14c3cec4ee0f76a5bc16bbc50e6bcf726ca01904b0f4e7154ef06791260be1127259d4eae5044665d07329e18a6308f8028c0d3dea773b0b905ba29ae6f2f683cce6040a4391c0fb03731ce7912edaeac97df5b44bce0a1d82cb3c9a21d48869f10bb11572b7ce1e051d164900a2b2b3bb6c36626f410b9e5b65e57a206983c779b77ce6ab15fa0f9078b4ac9ee578731018fc981341d5a95103767c7cf24b93cdbe1f8644622fe4f5472522b2d63760f504954c63e496d353acd777f9aea905b65b9bb419a67cc967b0fac6872389b7f737f9038fabb526cbbdee38238d51953f372fdd9ebc260b0ee2e391420c4b11145bd439954834d9a79e78abc57e03d3ee20d239d8a13014976e3f057ab3c38ca79ee82ebd2f9fd2aa46f2a36d6fffe4715c2bdfcc1958136742946bc5993d5b51e947" +} diff --git a/cmd/example-data/onion5.json b/cmd/example-data/onion5.json new file mode 100644 index 0000000..50221c2 --- /dev/null +++ b/cmd/example-data/onion5.json @@ -0,0 +1,5 @@ +{ + "session_key": "4545454545454545454545454545454545454545454545454545454545454545", + "associated_data": "4242424242424242424242424242424242424242424242424242424242424242", + "onion": "0003a214ebd875aab6ddfd77f22c5e7311d7f77f17a169e599f157bbcdae8bf071f40bacffa7597e025eea9286b58117e0cd9250c5800ba52f9342e43477eac9c03bed26ea0d0e31f62c1ee71220d71ed638f95b2c2fe332371c59abd7e934d423437744e6237aa9eb7d373ac72d44c33e5e7e53f5c7e09264d712a3cc1d757e6f48c0604f08297d12839030b09e0a69137cb717260b590edbffd98b2dc31a87cf21bb1d5dfb3e2abcc89846ce5eac9c404d6ada0c84bb68bb3fcb585aef8f689ee206ce12587a4a0ead0cbf445c13f04e8f76c62a9c3d3312185bca81bdf0d46e8733645912c0ae31d8707a772a6bc179f62724461553acdc4e54f4b90d6c583889e07d0ec6c27a11d090122a1d075e5094063d6696078a37f34dd621847cf8965baecbfff2094c31a5da449e4cfea9fcd0438ed29fb8c1c3006b7c047055b12407925ea74c2bb09ca21f4bc9d39dc4a7237ef6facbb15f2c10ccfeefc52524019ad0c33bf62729e98130384c2f5ec4e68fae784f71674ba9fd2a89c328695e582d3bc850dc0e1fe86ddb88138a56df283dc380f5114e3a3970f71eac063e5447ec37777691e56e0f79e372eeed6bfb7884cf9c5dd6495c2d7fe56bd1c6d112a69b078e0f82b9096ec322a7951318a23c2b06e78c3c0bd4aa76f8da1f7b285159d3f89bc47993d9a4d9759d7db1f2b1ca9b3629da41d647d7a4451801e66fd44670d4881fecce3bc6e9c09d4f7a4c5c6ffbb248c3e29cb9a5d7a75490f2f5ed28a7241139588b64bb692f92241e312de035d21f3a52269693cd985aecf6e80eb80779ac73bb3814809b4434291e423c2caeb91b3f0f744425a7f2640c7cd481b9808e6251f963020ae3403c0c719aa8a06207b3d92e65ea39f9b1a6637b9f71a4ec8d9b38cf42a6bcb02ee771fbba765e852d21398b1612328a54c50f165efebc5d258edbc02e1deaaaffb201c61bda6a85eb802aaec66d71a8121c7520127680d11a6a3017722af48b0bf796fa39c37bfde2217f896f764eded018b096671f63f066dbc7018d23966be95b244b5eaf81cbea0edbbb75bfc31c12a63c5262b8b1468ae4282112f8531bb8dfa45ac5b62dd01039737e808074ebdf0a49c73a30462bba152c778ae63c90413333e508225c1890983798b1bbd2fe75da1236e8752db4ad8abba0161255fbc47dcc07e1b213f8f78a75b8d5ba7041e294a30f6cf558f73ac6ec48046a087946dbc2612116d1fe2deaeacc24eda2277d5bf92892468590f5d2e467cff584aad2efaa601f30e1958afc1d84efca83e2bc55ec6bbe3b7f0981c5f46b3bbc39f71754dcc965b1b3e6972f7aa18914cb27675af3137b8421cded725a2cb1e73ec6a6a149ab6647babeae10f26d34fce7ef961c2790124cdb2d9f23aa3caaf3d52cac0b1bfc1b9c8ae7df401bd051ad89b5a0ae7352dd9e8f029ff080d856d07b44a5b430875f89849bac2aeea155573f59f09be57de93f1169964647d58b21c1d925d0ee2277cd11e129090a019a4a193563c2051ed1ca1b29bfa72612293713110939392056abcd296e54dc6dfcfedaea1c4d7358293e9dc6cf381cc8dea6cf90705700ce7a0555ef2142b061c02ead628fc8f0ddc2409ebda4fbb275d4c44fc8644a4431628f4afcb2afdc8de0e0e1ecfe7fc81d65ee4a1554587be83b82f391e61be12c18ab7b695d3f2284291b56bae638d098411cdfa27915f128d39727e5a8f315fea61937efb1a5f41f2a5b74e0ce78c401e5f55fa3451ed4f60469a135a3dea7dad750f39082b3d9a7477d74963aaefcf22c9c02d50e36731a0b72092f8d5bc8cd346762e93b2bf203d00264e4bc136fc142de8f7b69154deb05854ea88e2d7506222c95ba1aab065cf435b4dc1968a05e5471b20147684e96c4de4894a0543647a046d8c92abebd08" +} diff --git a/cmd/main.go b/cmd/main.go index 5266555..8d17c30 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -5,160 +5,352 @@ import ( "encoding/hex" "encoding/json" "fmt" - "io/ioutil" "log" "os" - "strings" "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/chaincfg" sphinx "github.com/lightningnetwork/lightning-onion" + "github.com/urfave/cli" ) -type OnionHopSpec struct { - Realm int `json:"realm"` - PublicKey string `json:"pubkey"` - Payload string `json:"payload"` -} +const ( + defaultHopDataPath = "cmd/example-data/hop-data.json" + defaultOnionPath = "cmd/example-data/onion.json" +) -type OnionSpec struct { - SessionKey string `json:"session_key,omitempty"` - Hops []OnionHopSpec `json:"hops"` -} +// main implements a simple command line utility that can be used in order to +// either generate a fresh mix-header or decode and fully process an existing +// one given a private key. +func main() { + app := cli.NewApp() + app.Name = "sphinx-cli" + app.Commands = []cli.Command{ + { + Name: "genkeys", + Usage: "A helper function to generate a random new " + + "private-public key pair.", + Action: genKeys, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "priv", + Usage: "An optional flag to provide " + + "a private key. In this " + + "case, this command just " + + "calculates and prints the " + + "associated public key", + }, + }, + }, + { + Name: "nextephemeral", + Usage: "A helper to compute the next ephemeral key " + + "given the current ephemeral key and a " + + "private key", + Action: nextEphemeral, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "priv", + Required: true, + }, + cli.StringFlag{ + Name: "pub", + Required: true, + }, + }, + }, + { + Name: "generate", + Usage: "Build a new onion.", + Action: generate, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "file", + Usage: "Path to json file containing " + + "the session key and hops " + + "data.", + Value: defaultHopDataPath, + }, + }, + }, + { + Name: "peel", + Usage: "Peel the onion.", + Action: peel, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "file", + Usage: "Path to json file containing " + + "the onion to decode along " + + "with the session key and " + + "associated data.", + Value: defaultOnionPath, + }, + }, + }, + } -func parseOnionSpec(spec OnionSpec) (*sphinx.PaymentPath, *btcec.PrivateKey, error) { - var path sphinx.PaymentPath - var binSessionKey []byte - var err error + if err := app.Run(os.Args); err != nil { + log.Fatalln(err) + } +} - if spec.SessionKey != "" { - binSessionKey, err = hex.DecodeString(spec.SessionKey) +func genKeys(cli *cli.Context) error { + var ( + priv *btcec.PrivateKey + pub *btcec.PublicKey + err error + ) + if privKeyStr := cli.String("priv"); privKeyStr != "" { + privBytes, err := hex.DecodeString(privKeyStr) if err != nil { - log.Fatalf("Unable to decode the sessionKey %v: %v\n", spec.SessionKey, err) + return err } + priv, pub = btcec.PrivKeyFromBytes(privBytes) - if len(binSessionKey) != 32 { - log.Fatalf("Session key must be a 32 byte hex string: %v\n", spec.SessionKey) - } } else { - binSessionKey = bytes.Repeat([]byte{'A'}, 32) + priv, err = btcec.NewPrivateKey() + if err != nil { + return err + } + + pub = priv.PubKey() } - sessionKey, _ := btcec.PrivKeyFromBytes(binSessionKey) + fmt.Printf("Private Key: %x\nPublic Key: %x\n", priv.Serialize(), + pub.SerializeCompressed()) + + return nil +} + +type pathData struct { + SessionKey string `json:"session_key"` + AssociatedData string `json:"associated_data"` + Hops []hopData `json:"hops"` +} - for i, hop := range spec.Hops { +type hopData struct { + PublicKey string `json:"pubkey"` + Payload string `json:"payload"` +} + +func parsePathData(data pathData) (*sphinx.PaymentPath, *btcec.PrivateKey, + []byte, error) { + + sessionKeyBytes, err := hex.DecodeString(data.SessionKey) + if err != nil { + return nil, nil, nil, fmt.Errorf("unable to decode the "+ + "sessionKey %v: %v", data.SessionKey, err) + } + + if len(sessionKeyBytes) != 32 { + return nil, nil, nil, fmt.Errorf("session priv key must be " + + "32 bytes long") + } + + sessionKey, _ := btcec.PrivKeyFromBytes(sessionKeyBytes) + + assocData, err := hex.DecodeString(data.AssociatedData) + if err != nil { + return nil, nil, nil, fmt.Errorf("unable to decode the "+ + "associate data %v: %v", data.AssociatedData, err) + } + + var path sphinx.PaymentPath + for i, hop := range data.Hops { binKey, err := hex.DecodeString(hop.PublicKey) - if err != nil || len(binKey) != 33 { - log.Fatalf("%s is not a valid hex pubkey %s", hop.PublicKey, err) + if err != nil { + return nil, nil, nil, err } pubkey, err := btcec.ParsePubKey(binKey) if err != nil { - log.Fatalf("%s is not a valid hex pubkey %s", hop.PublicKey, err) + return nil, nil, nil, err } path[i].NodePub = *pubkey payload, err := hex.DecodeString(hop.Payload) if err != nil { - log.Fatalf("%s is not a valid hex payload %s", - hop.Payload, err) + return nil, nil, nil, err } - hopPayload, err := sphinx.NewHopPayload(nil, payload) + hopPayload, err := sphinx.NewTLVHopPayload(payload) if err != nil { - log.Fatalf("unable to make payload: %v", err) + return nil, nil, nil, err } path[i].HopPayload = hopPayload + } - fmt.Fprintf(os.Stderr, "Node %d pubkey %x\n", i, pubkey.SerializeCompressed()) + return &path, sessionKey, assocData, nil +} + +func generate(ctx *cli.Context) error { + file := ctx.String("file") + jsonSpec, err := os.ReadFile(file) + if err != nil { + return fmt.Errorf("unable to read JSON onion spec from file "+ + "%v: %v", file, err) + } + + var spec pathData + if err := json.Unmarshal(jsonSpec, &spec); err != nil { + return fmt.Errorf("unable to peel JSON onion spec: %v", err) + } + + path, sessionKey, assocData, err := parsePathData(spec) + if err != nil { + return fmt.Errorf("could not peel onion spec: %v", err) } - return &path, sessionKey, nil + + msg, err := sphinx.NewOnionPacket( + path, sessionKey, assocData, sphinx.DeterministicPacketFiller, + ) + if err != nil { + return fmt.Errorf("error creating message: %v", err) + } + + w := bytes.NewBuffer([]byte{}) + err = msg.Encode(w) + if err != nil { + return fmt.Errorf("error serializing message: %v", err) + } + + fmt.Printf("%x\n", w.Bytes()) + return nil } -// main implements a simple command line utility that can be used in order to -// either generate a fresh mix-header or decode and fully process an existing -// one given a private key. -func main() { - args := os.Args +type onionInfo struct { + SessionKey string `json:"session_key"` + AssociatedData string `json:"associated_data"` + BlindingPoint string `json:"blinding_point"` + Onion string `json:"onion"` +} - assocData := bytes.Repeat([]byte{'B'}, 32) +func parseOnionInfo(info *onionInfo) (*sphinx.OnionPacket, *btcec.PrivateKey, + []byte, *btcec.PublicKey, error) { - if len(args) < 3 { - fmt.Printf("Usage: %s (generate|decode) \n", args[0]) - return - } else if args[1] == "generate" { - var spec OnionSpec + sessionKeyBytes, err := hex.DecodeString(info.SessionKey) + if err != nil { + return nil, nil, nil, nil, fmt.Errorf("unable to decode the "+ + "sessionKey %v: %v", info.SessionKey, err) + } - jsonSpec, err := ioutil.ReadFile(args[2]) - if err != nil { - log.Fatalf("Unable to read JSON onion spec from file %v: %v", args[2], err) - } + if len(sessionKeyBytes) != 32 { + return nil, nil, nil, nil, fmt.Errorf("session priv key must " + + "be 32 bytes long") + } - if err := json.Unmarshal(jsonSpec, &spec); err != nil { - log.Fatalf("Unable to parse JSON onion spec: %v", err) - } + sessionKey, _ := btcec.PrivKeyFromBytes(sessionKeyBytes) - path, sessionKey, err := parseOnionSpec(spec) - if err != nil { - log.Fatalf("could not parse onion spec: %v", err) - } + assocData, err := hex.DecodeString(info.AssociatedData) + if err != nil { + return nil, nil, nil, nil, fmt.Errorf("unable to decode the "+ + "associate data %v: %v", info.AssociatedData, err) + } + + onion, err := hex.DecodeString(info.Onion) + if err != nil { + return nil, nil, nil, nil, fmt.Errorf("unable to decode the "+ + "onion %v: %v", info.Onion, err) + } - msg, err := sphinx.NewOnionPacket( - path, sessionKey, assocData, - sphinx.DeterministicPacketFiller, - ) + var packet sphinx.OnionPacket + err = packet.Decode(bytes.NewBuffer(onion)) + if err != nil { + return nil, nil, nil, nil, err + } + + var blindingPoint *btcec.PublicKey + if info.BlindingPoint != "" { + bpBytes, err := hex.DecodeString(info.BlindingPoint) if err != nil { - log.Fatalf("Error creating message: %v", err) + return nil, nil, nil, nil, err } - w := bytes.NewBuffer([]byte{}) - err = msg.Encode(w) + blindingPoint, err = btcec.ParsePubKey(bpBytes) if err != nil { - log.Fatalf("Error serializing message: %v", err) + return nil, nil, nil, nil, err } + } - fmt.Printf("%x\n", w.Bytes()) - } else if args[1] == "decode" { - binKey, err := hex.DecodeString(args[2]) - if len(binKey) != 32 || err != nil { - log.Fatalf("Argument not a valid hex private key") - } + return &packet, sessionKey, assocData, blindingPoint, nil +} - hexBytes, _ := ioutil.ReadAll(os.Stdin) - binMsg, err := hex.DecodeString(strings.TrimSpace(string(hexBytes))) - if err != nil { - log.Fatalf("Error decoding message: %s", err) - } +func peel(ctx *cli.Context) error { + file := ctx.String("file") + jsonSpec, err := os.ReadFile(file) + if err != nil { + return fmt.Errorf("unable to read JSON onion spec from file "+ + "%v: %v", file, err) + } - privkey, _ := btcec.PrivKeyFromBytes(binKey) - privKeyECDH := &sphinx.PrivKeyECDH{PrivKey: privkey} - replayLog := sphinx.NewMemoryReplayLog() - s := sphinx.NewRouter( - privKeyECDH, &chaincfg.TestNet3Params, replayLog, - ) + var info onionInfo + if err := json.Unmarshal(jsonSpec, &info); err != nil { + return err + } - replayLog.Start() - defer replayLog.Stop() + packet, sessionKey, assocData, blindingPoint, err := parseOnionInfo( + &info, + ) + if err != nil { + return err + } - var packet sphinx.OnionPacket - err = packet.Decode(bytes.NewBuffer(binMsg)) + s := sphinx.NewRouter( + &sphinx.PrivKeyECDH{PrivKey: sessionKey}, + &chaincfg.TestNet3Params, sphinx.NewMemoryReplayLog(), + ) + s.Start() + defer s.Stop() - if err != nil { - log.Fatalf("Error parsing message: %v", err) - } - p, err := s.ProcessOnionPacket(&packet, assocData, 10) - if err != nil { - log.Fatalf("Failed to decode message: %s", err) - } + p, err := s.ProcessOnionPacket( + packet, assocData, 10, sphinx.WithBlindingPoint(blindingPoint), + ) + if err != nil { + return err + } - w := bytes.NewBuffer([]byte{}) - err = p.NextPacket.Encode(w) + w := bytes.NewBuffer([]byte{}) + if err = p.NextPacket.Encode(w); err != nil { + return fmt.Errorf("error serializing message: %v", err) + } - if err != nil { - log.Fatalf("Error serializing message: %v", err) - } - fmt.Printf("%x\n", w.Bytes()) + fmt.Printf("%x\n", w.Bytes()) + + return nil +} + +func nextEphemeral(ctx *cli.Context) error { + privKeyByte, err := hex.DecodeString(ctx.String("priv")) + if err != nil { + return err + } + if len(privKeyByte) != 32 { + return fmt.Errorf("private key must be 32 bytes") + } + + privKey, _ := btcec.PrivKeyFromBytes(privKeyByte) + + pubKeyBytes, err := hex.DecodeString(ctx.String("pub")) + if err != nil { + return err } + + pubKey, err := btcec.ParsePubKey(pubKeyBytes) + if err != nil { + return err + } + + nextBlindedKey, err := sphinx.NextEphemeral( + &sphinx.PrivKeyECDH{PrivKey: privKey}, pubKey, + ) + if err != nil { + return err + } + + fmt.Printf("%x\n", nextBlindedKey.SerializeCompressed()) + + return nil } diff --git a/crypto.go b/crypto.go index 0930544..73fbed2 100644 --- a/crypto.go +++ b/crypto.go @@ -10,6 +10,7 @@ import ( "github.com/aead/chacha20" "github.com/btcsuite/btcd/btcec/v2" secp "github.com/decred/dcrd/dcrec/secp256k1/v4" + "golang.org/x/crypto/chacha20poly1305" ) const ( @@ -19,6 +20,10 @@ const ( HMACSize = 32 ) +// chaChaPolyZeroNonce is a slice of zero bytes used in the chacha20poly1305 +// encryption and decryption. +var chaChaPolyZeroNonce [chacha20poly1305.NonceSize]byte + // Hash256 is a statically sized, 32-byte array, typically containing // the output of a SHA256 hash. type Hash256 [sha256.Size]byte @@ -61,8 +66,8 @@ func (p *PrivKeyECDH) PubKey() *btcec.PublicKey { // k is our private key, and P is the public key, we perform the following // operation: // -// sx := k*P -// s := sha256(sx.SerializeCompressed()) +// sx := k*P +// s := sha256(sx.SerializeCompressed()) // // NOTE: This is part of the SingleKeyECDH interface. func (p *PrivKeyECDH) ECDH(pub *btcec.PublicKey) ([32]byte, error) { @@ -199,6 +204,31 @@ func blindBaseElement(blindingFactor btcec.ModNScalar) *btcec.PublicKey { return priv.PubKey() } +// chacha20polyEncrypt initialises the ChaCha20Poly1305 algorithm with the given +// key and uses it to encrypt the passed message. This uses an all-zero nonce as +// required by the route-blinding spec. +func chacha20polyEncrypt(key, plainTxt []byte) ([]byte, error) { + aead, err := chacha20poly1305.New(key) + if err != nil { + return nil, err + } + + return aead.Seal(plainTxt[:0], chaChaPolyZeroNonce[:], plainTxt, nil), + nil +} + +// chacha20polyDecrypt initialises the ChaCha20Poly1305 algorithm with the given +// key and uses it to decrypt the passed cipher text. This uses an all-zero +// nonce as required by the route-blinding spec. +func chacha20polyDecrypt(key, cipherTxt []byte) ([]byte, error) { + aead, err := chacha20poly1305.New(key) + if err != nil { + return nil, err + } + + return aead.Open(cipherTxt[:0], chaChaPolyZeroNonce[:], cipherTxt, nil) +} + // sharedSecretGenerator is an interface that abstracts away exactly *how* the // shared secret for each hop is generated. // @@ -209,17 +239,63 @@ type sharedSecretGenerator interface { generateSharedSecret(dhKey *btcec.PublicKey) (Hash256, error) } -// generateSharedSecret generates the shared secret by given ephemeral key. -func (r *Router) generateSharedSecret(dhKey *btcec.PublicKey) (Hash256, error) { +// generateSharedSecret generates the shared secret using the given ephemeral +// pub key and the Router's private key. If a blindingPoint is provided then it +// is used to tweak the Router's private key before creating the shared secret +// with the ephemeral pub key. The blinding point is used to determine our +// shared secret with the receiver. From that we can determine our shared +// secret with the sender using the dhKey. +func (r *Router) generateSharedSecret(dhKey, + blindingPoint *btcec.PublicKey) (Hash256, error) { + + // If no blinding point is provided, then the un-tweaked dhKey can + // be used to derive the shared secret + if blindingPoint == nil { + return sharedSecret(r.onionKey, dhKey) + } + + // We use the blinding point to calculate the blinding factor that the + // receiver used with us so that we can use it to tweak our priv key. + // The sender would have created their shared secret with our blinded + // pub key. + // * ss_receiver = H(k * E_receiver) + ssReceiver, err := sharedSecret(r.onionKey, blindingPoint) + if err != nil { + return Hash256{}, err + } + + // Compute the blinding factor that the receiver would have used to + // blind our public key. + // + // * bf = HMAC256("blinded_node_id", ss_receiver) + blindingFactorBytes := generateKey(routeBlindingHMACKey, &ssReceiver) + var blindingFactor btcec.ModNScalar + blindingFactor.SetBytes(&blindingFactorBytes) + + // Now, we want to calculate the shared secret between the sender and + // our blinded key. In other words we want to calculate: + // * ss_sender = H(E_sender * bf * k) + // + // Since the order in which the above multiplication happens does not + // matter, we will first multiply E_sender with the blinding factor: + blindedEphemeral := blindGroupElement(dhKey, blindingFactor) + + // Finally, we compute the ECDH to get the shared secret, ss_sender: + return sharedSecret(r.onionKey, blindedEphemeral) +} + +// sharedSecret does a ECDH operation on the passed private and public keys and +// returns the result. +func sharedSecret(priv SingleKeyECDH, pub *btcec.PublicKey) (Hash256, error) { var sharedSecret Hash256 // Ensure that the public key is on our curve. - if !dhKey.IsOnCurve() { + if !pub.IsOnCurve() { return sharedSecret, ErrInvalidOnionKey } - // Compute our shared secret. - return r.onionKey.ECDH(dhKey) + // Compute the shared secret. + return priv.ECDH(pub) } // onionEncrypt obfuscates the data with compliance with BOLT#4. As we use a diff --git a/go.mod b/go.mod index d274362..db36ece 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,18 @@ require ( github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f 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 ) -go 1.13 +require ( + github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d // 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 + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +go 1.19 diff --git a/go.sum b/go.sum index fe83050..e8e4f31 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,4 @@ +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/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= @@ -20,7 +21,10 @@ github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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= @@ -53,6 +57,21 @@ github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5 github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +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/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/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-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 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= @@ -85,9 +104,14 @@ google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQ google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/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= diff --git a/obfuscation.go b/obfuscation.go index b8df7cc..a731a6c 100644 --- a/obfuscation.go +++ b/obfuscation.go @@ -13,12 +13,18 @@ type OnionErrorEncrypter struct { } // NewOnionErrorEncrypter creates new instance of the onion encrypter backed by -// the passed router, with encryption to be doing using the passed -// ephemeralKey. -func NewOnionErrorEncrypter(router *Router, - ephemeralKey *btcec.PublicKey) (*OnionErrorEncrypter, error) { +// the passed router, with encryption to be done using the passed ephemeralKey. +func NewOnionErrorEncrypter(router *Router, ephemeralKey *btcec.PublicKey, + opts ...ProcessOnionOpt) (*OnionErrorEncrypter, error) { - sharedSecret, err := router.generateSharedSecret(ephemeralKey) + cfg := &processOnionCfg{} + for _, o := range opts { + o(cfg) + } + + sharedSecret, err := router.generateSharedSecret( + ephemeralKey, cfg.blindingPoint, + ) if err != nil { return nil, err } diff --git a/path.go b/path.go index f46ba01..6b8e839 100644 --- a/path.go +++ b/path.go @@ -1,319 +1,27 @@ package sphinx import ( - "bufio" - "bytes" - "encoding/binary" + "errors" "fmt" - "io" "github.com/btcsuite/btcd/btcec/v2" - "github.com/btcsuite/btcd/wire" ) -// HopData is the information destined for individual hops. It is a fixed size -// 64 bytes, prefixed with a 1 byte realm that indicates how to interpret it. -// For now we simply assume it's the bitcoin realm (0x00) and hence the format -// is fixed. The last 32 bytes are always the HMAC to be passed to the next -// hop, or zero if this is the packet is not to be forwarded, since this is the -// last hop. -type HopData struct { - // Realm denotes the "real" of target chain of the next hop. For - // bitcoin, this value will be 0x00. - Realm [RealmByteSize]byte - - // NextAddress is the address of the next hop that this packet should - // be forward to. - NextAddress [AddressSize]byte - - // ForwardAmount is the HTLC amount that the next hop should forward. - // This value should take into account the fee require by this - // particular hop, and the cumulative fee for the entire route. - ForwardAmount uint64 - - // OutgoingCltv is the value of the outgoing absolute time-lock that - // should be included in the HTLC forwarded. - OutgoingCltv uint32 - - // ExtraBytes is the set of unused bytes within the onion payload. This - // extra set of bytes can be utilized by higher level applications to - // package additional data within the per-hop payload, or signal that a - // portion of the remaining set of hops are to be consumed as Extra - // Onion Blobs. - // - // TODO(roasbeef): rename to padding bytes? - ExtraBytes [NumPaddingBytes]byte -} - -// Encode writes the serialized version of the target HopData into the passed -// io.Writer. -func (hd *HopData) Encode(w io.Writer) error { - if _, err := w.Write(hd.Realm[:]); err != nil { - return err - } - - if _, err := w.Write(hd.NextAddress[:]); err != nil { - return err - } - - if err := binary.Write(w, binary.BigEndian, hd.ForwardAmount); err != nil { - return err - } - - if err := binary.Write(w, binary.BigEndian, hd.OutgoingCltv); err != nil { - return err - } - - if _, err := w.Write(hd.ExtraBytes[:]); err != nil { - return err - } - - return nil -} - -// Decodes populates the target HopData with the contents of a serialized -// HopData packed into the passed io.Reader. -func (hd *HopData) Decode(r io.Reader) error { - if _, err := io.ReadFull(r, hd.Realm[:]); err != nil { - return err - } - - if _, err := io.ReadFull(r, hd.NextAddress[:]); err != nil { - return err - } - - err := binary.Read(r, binary.BigEndian, &hd.ForwardAmount) - if err != nil { - return err - } - - err = binary.Read(r, binary.BigEndian, &hd.OutgoingCltv) - if err != nil { - return err - } - - _, err = io.ReadFull(r, hd.ExtraBytes[:]) - return err -} - -// PayloadType denotes the type of the payload included in the onion packet. -// Serialization of a raw HopPayload will depend on the payload type, as some -// include a varint length prefix, while others just encode the raw payload. -type PayloadType uint8 - const ( - // PayloadLegacy is the legacy payload type. It includes a fixed 32 - // bytes, 12 of which are padding, and uses a "zero length" (the old - // realm) prefix. - PayloadLegacy PayloadType = iota - - // PayloadTLV is the new modern TLV based format. This payload includes - // a set of opaque bytes with a varint length prefix. The varint used - // is the same CompactInt as used in the Bitcoin protocol. - PayloadTLV + // NumMaxHops is the maximum path length. There is a maximum of 1300 + // bytes in the routing info block. Legacy hop payloads are always 65 + // bytes, while tlv payloads are at least 47 bytes (tlvlen 1, amt 2, + // timelock 2, nextchan 10, hmac 32) for the intermediate hops and 37 + // bytes (tlvlen 1, amt 2, timelock 2, hmac 32) for the exit hop. The + // maximum path length can therefore only be reached by using tlv + // payloads only. With that, the maximum number of intermediate hops + // is: Floor((1300 - 37) / 47) = 26. Including the exit hop, the + // maximum path length is 27 hops. + NumMaxHops = 27 + + routeBlindingHMACKey = "blinded_node_id" ) -// HopPayload is a slice of bytes and associated payload-type that are destined -// for a specific hop in the PaymentPath. The payload itself is treated as an -// opaque data field by the onion router. The included Type field informs the -// serialization/deserialziation of the raw payload. -type HopPayload struct { - // Type is the type of the payload. - Type PayloadType - - // Payload is the raw bytes of the per-hop payload for this hop. - // Depending on the realm, this pay be the regular legacy hop data, or - // a set of opaque blobs to be parsed by higher layers. - Payload []byte - - // HMAC is an HMAC computed over the entire per-hop payload that also - // includes the higher-level (optional) associated data bytes. - HMAC [HMACSize]byte -} - -// NewHopPayload creates a new hop payload given an optional set of forwarding -// instructions for a hop, and a set of optional opaque extra onion bytes to -// drop off at the target hop. If both values are not specified, then an error -// is returned. -func NewHopPayload(hopData *HopData, eob []byte) (HopPayload, error) { - var ( - h HopPayload - b bytes.Buffer - ) - - // We can't proceed if neither the hop data or the EOB has been - // specified by the caller. - switch { - case hopData == nil && len(eob) == 0: - return h, fmt.Errorf("either hop data or eob must " + - "be specified") - - case hopData != nil && len(eob) > 0: - return h, fmt.Errorf("cannot provide both hop data AND an eob") - - } - - // If the hop data is specified, then we'll write that now, as it - // should proceed the EOB portion of the payload. - if hopData != nil { - if err := hopData.Encode(&b); err != nil { - return h, nil - } - - // We'll also mark that this particular hop will be using the - // legacy format as the modern format packs the existing hop - // data information into the EOB space as a TLV stream. - h.Type = PayloadLegacy - } else { - // Otherwise, we'll write out the raw EOB which contains a set - // of opaque bytes that the recipient can decode to make a - // forwarding decision. - if _, err := b.Write(eob); err != nil { - return h, nil - } - - h.Type = PayloadTLV - } - - h.Payload = b.Bytes() - - return h, nil -} - -// NumBytes returns the number of bytes it will take to serialize the full -// payload. Depending on the payload type, this may include some additional -// signalling bytes. -func (hp *HopPayload) NumBytes() int { - // The base size is the size of the raw payload, and the size of the - // HMAC. - size := len(hp.Payload) + HMACSize - - // If this is the new TLV format, then we'll also accumulate the number - // of bytes that it would take to encode the size of the payload. - if hp.Type == PayloadTLV { - payloadSize := len(hp.Payload) - size += int(wire.VarIntSerializeSize(uint64(payloadSize))) - } - - return size -} - -// Encode encodes the hop payload into the passed writer. -func (hp *HopPayload) Encode(w io.Writer) error { - switch hp.Type { - - // For the legacy payload, we don't need to add any additional bytes as - // our realm byte serves as our zero prefix byte. - case PayloadLegacy: - break - - // For the TLV payload, we'll first prepend the length of the payload - // as a var-int. - case PayloadTLV: - var b [8]byte - err := WriteVarInt(w, uint64(len(hp.Payload)), &b) - if err != nil { - return err - } - } - - // Finally, we'll write out the raw payload, then the HMAC in series. - if _, err := w.Write(hp.Payload); err != nil { - return err - } - if _, err := w.Write(hp.HMAC[:]); err != nil { - return err - } - - return nil -} - -// Decode unpacks an encoded HopPayload from the passed reader into the target -// HopPayload. -func (hp *HopPayload) Decode(r io.Reader) 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. - peekByte, err := bufReader.Peek(1) - if err != nil { - return err - } - - var payloadSize uint32 - - switch int(peekByte[0]) { - // If the first byte is a zero (the realm), then this is the normal - // payload. - case 0x00: - // Our size is just the payload, without the HMAC. This means - // that this is the legacy payload type. - payloadSize = LegacyHopDataSize - HMACSize - hp.Type = PayloadLegacy - - default: - // Otherwise, this is the new TLV based payload type, so we'll - // extract the payload length encoded as a var-int. - var b [8]byte - varInt, err := ReadVarInt(bufReader, &b) - if err != nil { - return err - } - - payloadSize = uint32(varInt) - hp.Type = PayloadTLV - } - - // 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 - } - if _, err := io.ReadFull(bufReader, hp.HMAC[:]); err != nil { - return err - } - - return nil -} - -// HopData attempts to extract a set of forwarding instructions from the target -// HopPayload. If the realm isn't what we expect, then an error is returned. -// This method also returns the left over EOB that remain after the hop data -// has been parsed. Callers may want to map this blob into something more -// concrete. -func (hp *HopPayload) HopData() (*HopData, error) { - payloadReader := bytes.NewBuffer(hp.Payload) - - // If this isn't the "base" realm, then we can't extract the expected - // hop payload structure from the payload. - if hp.Type != PayloadLegacy { - return nil, nil - } - - // Now that we know the payload has the structure we expect, we'll - // decode the payload into the HopData. - var hd HopData - if err := hd.Decode(payloadReader); err != nil { - return nil, err - } - - return &hd, nil -} - -// NumMaxHops is the maximum path length. There is a maximum of 1300 bytes in -// the routing info block. Legacy hop payloads are always 65 bytes, while tlv -// payloads are at least 47 bytes (tlvlen 1, amt 2, timelock 2, nextchan 10, -// hmac 32) for the intermediate hops and 37 bytes (tlvlen 1, amt 2, timelock 2, -// hmac 32) for the exit hop. The maximum path length can therefore only be -// reached by using tlv payloads only. With that, the maximum number of -// intermediate hops is: Floor((1300 - 37) / 47) = 26. Including the exit hop, -// the maximum path length is 27 hops. -const NumMaxHops = 27 - // PaymentPath represents a series of hops within the Lightning Network // starting at a sender and terminating at a receiver. Each hop contains a set // of mandatory data which contains forwarding instructions for that hop. @@ -393,3 +101,158 @@ func (p *PaymentPath) TotalPayloadSize() int { return totalSize } + +// BlindedPath represents all the data that the creator of a blinded path must +// transmit to the builder of route that will send to this path. +type BlindedPath struct { + // IntroductionPoint is the real node ID of the first hop in the blinded + // path. The sender should be able to find this node in the network + // graph and route to it. + IntroductionPoint *btcec.PublicKey + + // BlindingPoint is the first ephemeral blinding point. This is the + // point that the introduction node will use in order to create a shared + // secret with the builder of the blinded route. This point will need + // to be communicated to the introduction node by the sender in some + // way. + BlindingPoint *btcec.PublicKey + + // BlindedHops is a list of ordered BlindedHopInfo. Each entry + // represents a hop in the blinded path along with the encrypted data to + // be sent to that node. Note that the first entry in the list + // represents the introduction point of the path and so the node ID of + // this point does not strictly need to be transmitted to the sender + // since they will be able to derive the point using the BlindingPoint. + BlindedHops []*BlindedHopInfo +} + +// BlindedHopInfo represents a blinded node pub key along with the encrypted +// data for a node in a blinded route. +type BlindedHopInfo struct { + // BlindedNodePub is the blinded public key of the node in the blinded + // route. + BlindedNodePub *btcec.PublicKey + + // CipherText is the encrypted payload to be transported to the hop in + // the blinded route. + CipherText []byte +} + +// HopInfo represents a real node pub key along with the plaintext data for a +// node in a blinded route. +type HopInfo struct { + // NodePub is the real public key of the node in the blinded route. + NodePub *btcec.PublicKey + + // PlainText is the un-encrypted payload to be transported to the hop + // the blinded route. + PlainText []byte +} + +// Encrypt uses the given sharedSecret to blind the public key of the node and +// encrypt the payload and returns the resulting BlindedHopInfo. +func (i *HopInfo) Encrypt(sharedSecret Hash256) (*BlindedHopInfo, error) { + blindedData, err := encryptBlindedHopData(sharedSecret, i.PlainText) + if err != nil { + return nil, err + } + + return &BlindedHopInfo{ + BlindedNodePub: blindNodeID(sharedSecret, i.NodePub), + CipherText: blindedData, + }, nil +} + +// BuildBlindedPath creates a new BlindedPath from a session key along with a +// list of HopInfo representing the nodes in the blinded path. The first hop in +// paymentPath is expected to be the introduction node. +func BuildBlindedPath(sessionKey *btcec.PrivateKey, + paymentPath []*HopInfo) (*BlindedPath, error) { + + if len(paymentPath) < 1 { + return nil, errors.New("at least 1 hop is required to create " + + "a blinded path") + } + + bp := &BlindedPath{ + IntroductionPoint: paymentPath[0].NodePub, + BlindingPoint: sessionKey.PubKey(), + BlindedHops: make([]*BlindedHopInfo, len(paymentPath)), + } + + keys := make([]*btcec.PublicKey, len(paymentPath)) + for i, p := range paymentPath { + keys[i] = p.NodePub + } + + hopSharedSecrets, err := generateSharedSecrets(keys, sessionKey) + if err != nil { + return nil, fmt.Errorf("error generating shared secret: %v", + err) + } + + for i, hop := range paymentPath { + blindedInfo, err := hop.Encrypt(hopSharedSecrets[i]) + if err != nil { + return nil, err + } + + bp.BlindedHops[i] = blindedInfo + } + + return bp, nil +} + +// blindNodeID blinds the given public key using the provided shared secret. +func blindNodeID(sharedSecret Hash256, + pubKey *btcec.PublicKey) *btcec.PublicKey { + + blindingFactorBytes := generateKey(routeBlindingHMACKey, &sharedSecret) + + var blindingFactor btcec.ModNScalar + blindingFactor.SetBytes(&blindingFactorBytes) + + return blindGroupElement(pubKey, blindingFactor) +} + +// encryptBlindedHopData blinds/encrypts the given plain text data using the +// provided shared secret. +func encryptBlindedHopData(sharedSecret Hash256, plainTxt []byte) ([]byte, + error) { + + rhoKey := generateKey("rho", &sharedSecret) + + return chacha20polyEncrypt(rhoKey[:], plainTxt) +} + +// decryptBlindedHopData decrypts the data encrypted by the creator of the +// blinded route. +func decryptBlindedHopData(privKey SingleKeyECDH, ephemPub *btcec.PublicKey, + encryptedData []byte) ([]byte, error) { + + ss, err := privKey.ECDH(ephemPub) + if err != nil { + return nil, err + } + + ssHash := Hash256(ss) + rho := generateKey("rho", &ssHash) + + return chacha20polyDecrypt(rho[:], encryptedData) +} + +// NextEphemeral computes the next ephemeral key given the current ephemeral +// key and this node's private key. +func NextEphemeral(privKey SingleKeyECDH, + ephemPub *btcec.PublicKey) (*btcec.PublicKey, error) { + + ss, err := privKey.ECDH(ephemPub) + if err != nil { + return nil, err + } + + blindingFactor := computeBlindingFactor(ephemPub, ss[:]) + nextEphem := blindGroupElement(ephemPub, blindingFactor) + + return nextEphem, nil +} diff --git a/path_test.go b/path_test.go new file mode 100644 index 0000000..c61f145 --- /dev/null +++ b/path_test.go @@ -0,0 +1,286 @@ +package sphinx + +import ( + "bytes" + "encoding/hex" + "encoding/json" + "os" + "testing" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/chaincfg" + "github.com/stretchr/testify/require" +) + +const ( + routeBlindingTestFileName = "testdata/route-blinding-test.json" + onionRouteBlindingTestFileName = "testdata/onion-route-blinding-test.json" +) + +// TestBuildBlindedRoute tests BuildBlindedRoute and decryptBlindedHopData against +// the spec test vectors. +func TestBuildBlindedRoute(t *testing.T) { + t.Parallel() + + // First, we'll read out the raw Json file at the target location. + jsonBytes, err := os.ReadFile(routeBlindingTestFileName) + require.NoError(t, err) + + // Once we have the raw file, we'll unpack it into our + // blindingJsonTestCase struct defined below. + testCase := &blindingJsonTestCase{} + require.NoError(t, json.Unmarshal(jsonBytes, testCase)) + require.Len(t, testCase.Generate.Hops, 4) + + // buildPaymentPath is a helper closure used to convert hopData objects + // into BlindedPathHop objects. + buildPaymentPath := func(h []hopData) []*HopInfo { + path := make([]*HopInfo, len(h)) + for i, hop := range h { + nodeIDStr, _ := hex.DecodeString(hop.NodeID) + nodeID, _ := btcec.ParsePubKey(nodeIDStr) + payload, _ := hex.DecodeString(hop.EncodedTLVs) + + path[i] = &HopInfo{ + NodePub: nodeID, + PlainText: payload, + } + } + return path + } + + // First, Eve will build a blinded path from Dave to herself. + eveSessKey := privKeyFromString(testCase.Generate.Hops[2].SessionKey) + eveDavePath := buildPaymentPath(testCase.Generate.Hops[2:]) + pathED, err := BuildBlindedPath(eveSessKey, eveDavePath) + require.NoError(t, err) + + // At this point, Eve will give her blinded path to Bob who will then + // build his own blinded route from himself to Carol. He will then + // concatenate the two paths. Note that in his TLV for Carol, Bob will + // add the `next_blinding_override` field which he will set to the + // first blinding point in Eve's blinded route. This will indicate to + // Carol that she should use this point for the next blinding key + // instead of the next blinding key that she derives. + bobCarolPath := buildPaymentPath(testCase.Generate.Hops[:2]) + bobSessKey := privKeyFromString(testCase.Generate.Hops[0].SessionKey) + pathBC, err := BuildBlindedPath(bobSessKey, bobCarolPath) + require.NoError(t, err) + + // Construct the concatenated path. + path := &BlindedPath{ + IntroductionPoint: pathBC.IntroductionPoint, + BlindingPoint: pathBC.BlindingPoint, + BlindedHops: append(pathBC.BlindedHops, + pathED.BlindedHops...), + } + + // Check that the constructed path is equal to the test vector path. + require.True(t, equalPubKeys( + testCase.Route.IntroductionNodeID, path.IntroductionPoint, + )) + require.True(t, equalPubKeys( + testCase.Route.Blinding, path.BlindingPoint, + )) + + for i, hop := range testCase.Route.Hops { + require.True(t, equalPubKeys( + hop.BlindedNodeID, path.BlindedHops[i].BlindedNodePub, + )) + + data, _ := hex.DecodeString(hop.EncryptedData) + require.True( + t, bytes.Equal(data, path.BlindedHops[i].CipherText), + ) + } + + // Assert that each hop is able to decode the encrypted data meant for + // it. + for i, hop := range testCase.Unblind.Hops { + priv := privKeyFromString(hop.NodePrivKey) + ephem := pubKeyFromString(hop.EphemeralPubKey) + + data, err := decryptBlindedHopData( + &PrivKeyECDH{PrivKey: priv}, ephem, + path.BlindedHops[i].CipherText, + ) + require.NoError(t, err) + + decoded, _ := hex.DecodeString(hop.DecryptedData) + require.True(t, bytes.Equal(data, decoded)) + + nextEphem, err := NextEphemeral(&PrivKeyECDH{priv}, ephem) + require.NoError(t, err) + + require.True(t, equalPubKeys( + hop.NextEphemeralPubKey, nextEphem, + )) + } +} + +// TestOnionRouteBlinding tests that an onion packet can correctly be processed +// by a node in a blinded route. +func TestOnionRouteBlinding(t *testing.T) { + t.Parallel() + + // First, we'll read out the raw Json file at the target location. + jsonBytes, err := os.ReadFile(onionRouteBlindingTestFileName) + require.NoError(t, err) + + // Once we have the raw file, we'll unpack it into our + // blindingJsonTestCase struct defined above. + testCase := &onionBlindingJsonTestCase{} + require.NoError(t, json.Unmarshal(jsonBytes, testCase)) + + assoc, err := hex.DecodeString(testCase.Generate.AssocData) + require.NoError(t, err) + + // Extract the original onion packet to be processed. + onion, err := hex.DecodeString(testCase.Generate.Onion) + 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) *ProcessedPacket { + + r := NewRouter( + &PrivKeyECDH{PrivKey: key}, &chaincfg.MainNetParams, + NewMemoryReplayLog(), + ) + + require.NoError(t, r.Start()) + defer r.Stop() + + res, err := r.ProcessOnionPacket( + onionPacket, assoc, 10, + WithBlindingPoint(blindingPoint), + ) + require.NoError(t, err) + + return res + } + + hops := testCase.Decrypt.Hops + require.Len(t, hops, 5) + + // There are some things that the processor of the onion packet will + // only be able to determine from the actual contents of the encrypted + // data it receives. These things include the next_blinding_point for + // the introduction point and the next_blinding_override. The decryption + // of this data is dependent on the encoding chosen by higher layers. + // The test uses TLVs. Since the extraction of this data is dependent + // on layers outside the scope of this library, we provide handle these + // cases manually for the sake of the test. + var ( + introPointIndex = 2 + firstBlinding = pubKeyFromString(hops[1].NextBlinding) + + concatIndex = 3 + blindingOverride = pubKeyFromString(hops[2].NextBlinding) + ) + + var blindingPoint *btcec.PublicKey + for i, hop := range testCase.Decrypt.Hops { + buff := bytes.NewBuffer(nil) + require.NoError(t, onionPacket.Encode(buff)) + require.Equal(t, hop.Onion, hex.EncodeToString(buff.Bytes())) + + priv := privKeyFromString(hop.NodePrivKey) + + if i == introPointIndex { + blindingPoint = firstBlinding + } else if i == concatIndex { + blindingPoint = blindingOverride + } + + processedPkt := peelOnion(priv, blindingPoint) + + if blindingPoint != nil { + blindingPoint, err = NextEphemeral( + &PrivKeyECDH{priv}, blindingPoint, + ) + require.NoError(t, err) + } + onionPacket = processedPkt.NextPacket + } +} + +type onionBlindingJsonTestCase struct { + Generate generateOnionData `json:"generate"` + Decrypt decryptData `json:"decrypt"` +} + +type generateOnionData struct { + SessionKey string `json:"session_key"` + AssocData string `json:"associated_data"` + Onion string `json:"onion"` +} + +type decryptData struct { + Hops []decryptHops `json:"hops"` +} + +type decryptHops struct { + Onion string `json:"onion"` + NodePrivKey string `json:"node_privkey"` + NextBlinding string `json:"next_blinding"` +} + +type blindingJsonTestCase struct { + Generate generateData `json:"generate"` + Route routeData `json:"route"` + Unblind unblindData `json:"unblind"` +} + +type routeData struct { + IntroductionNodeID string `json:"introduction_node_id"` + Blinding string `json:"blinding"` + Hops []blindedHop `json:"hops"` +} + +type unblindData struct { + Hops []unblindedHop `json:"hops"` +} + +type generateData struct { + Hops []hopData `json:"hops"` +} + +type unblindedHop struct { + NodePrivKey string `json:"node_privkey"` + EphemeralPubKey string `json:"ephemeral_pubkey"` + DecryptedData string `json:"decrypted_data"` + NextEphemeralPubKey string `json:"next_ephemeral_pubkey"` +} + +type hopData struct { + SessionKey string `json:"session_key"` + NodeID string `json:"node_id"` + EncodedTLVs string `json:"encoded_tlvs"` +} + +type blindedHop struct { + BlindedNodeID string `json:"blinded_node_id"` + EncryptedData string `json:"encrypted_data"` +} + +func equalPubKeys(pkStr string, pk *btcec.PublicKey) bool { + return hex.EncodeToString(pk.SerializeCompressed()) == pkStr +} + +func privKeyFromString(pkStr string) *btcec.PrivateKey { + bytes, _ := hex.DecodeString(pkStr) + key, _ := btcec.PrivKeyFromBytes(bytes) + return key +} + +func pubKeyFromString(pkStr string) *btcec.PublicKey { + bytes, _ := hex.DecodeString(pkStr) + key, _ := btcec.ParsePubKey(bytes) + return key +} diff --git a/payload.go b/payload.go new file mode 100644 index 0000000..9e89dad --- /dev/null +++ b/payload.go @@ -0,0 +1,347 @@ +package sphinx + +import ( + "bufio" + "bytes" + "encoding/binary" + "fmt" + "io" + "math" + + "github.com/btcsuite/btcd/wire" +) + +// PayloadType denotes the type of the payload included in the onion packet. +// Serialization of a raw HopPayload will depend on the payload type, as some +// include a varint length prefix, while others just encode the raw payload. +type PayloadType uint8 + +const ( + // PayloadLegacy is the legacy payload type. It includes a fixed 32 + // bytes, 12 of which are padding, and uses a "zero length" (the old + // realm) prefix. + PayloadLegacy PayloadType = iota + + // PayloadTLV is the new modern TLV based format. This payload includes + // a set of opaque bytes with a varint length prefix. The varint used + // is the same CompactInt as used in the Bitcoin protocol. + PayloadTLV +) + +// HopPayload is a slice of bytes and associated payload-type that are destined +// for a specific hop in the PaymentPath. The payload itself is treated as an +// opaque data field by the onion router. The included Type field informs the +// serialization/deserialziation of the raw payload. +type HopPayload struct { + // Type is the type of the payload. + Type PayloadType + + // Payload is the raw bytes of the per-hop payload for this hop. + // Depending on the realm, this pay be the regular legacy hop data, or + // a set of opaque blobs to be parsed by higher layers. + Payload []byte + + // HMAC is an HMAC computed over the entire per-hop payload that also + // includes the higher-level (optional) associated data bytes. + HMAC [HMACSize]byte +} + +// NewTLVHopPayload creates a new TLV encoded HopPayload. The payload will be +// a TLV encoded stream that will contain forwarding instructions for a hop. +func NewTLVHopPayload(payload []byte) (HopPayload, error) { + var ( + h HopPayload + b bytes.Buffer + ) + + // Write out the raw payload which contains a set of opaque bytes that + // the recipient can decode to make a forwarding decision. + if _, err := b.Write(payload); err != nil { + return h, nil + } + + h.Type = PayloadTLV + h.Payload = b.Bytes() + + return h, nil +} + +// NumBytes returns the number of bytes it will take to serialize the full +// payload. Depending on the payload type, this may include some additional +// signalling bytes. +func (hp *HopPayload) NumBytes() int { + if hp.Type == PayloadLegacy { + return legacyNumBytes() + } + + return tlvNumBytes(len(hp.Payload)) +} + +// Encode encodes the hop payload into the passed writer. +func (hp *HopPayload) Encode(w io.Writer) error { + if hp.Type == PayloadLegacy { + return encodeLegacyHopPayload(hp, w) + } + + return encodeTLVHopPayload(hp, w) +} + +// Decode unpacks an encoded HopPayload from the passed reader into the target +// HopPayload. +func (hp *HopPayload) Decode(r io.Reader) 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. + peekByte, err := bufReader.Peek(1) + if err != nil { + return err + } + + var ( + legacyPayload = isLegacyPayloadByte(peekByte[0]) + payloadSize uint16 + ) + + if legacyPayload { + payloadSize = legacyPayloadSize() + hp.Type = PayloadLegacy + } else { + payloadSize, err = tlvPayloadSize(bufReader) + if err != nil { + return err + } + + hp.Type = PayloadTLV + } + + // 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 + } + if _, err := io.ReadFull(bufReader, hp.HMAC[:]); err != nil { + return err + } + + return nil +} + +// HopData attempts to extract a set of forwarding instructions from the target +// HopPayload. If the realm isn't what we expect, then an error is returned. +// This method also returns the left over EOB that remain after the hop data +// has been parsed. Callers may want to map this blob into something more +// concrete. +func (hp *HopPayload) HopData() (*HopData, error) { + // The HopData can only be extracted at this layer for payloads using + // the legacy encoding. + if hp.Type == PayloadLegacy { + return decodeLegacyHopData(hp.Payload) + } + + return nil, nil +} + +// tlvPayloadSize uses the passed reader to extract the payload length encoded +// as a var-int. +func tlvPayloadSize(r io.Reader) (uint16, error) { + var b [8]byte + varInt, err := ReadVarInt(r, &b) + if err != nil { + return 0, err + } + + if varInt > math.MaxUint16 { + return 0, fmt.Errorf("payload size of %d is larger than the "+ + "maximum allowed size of %d", varInt, math.MaxUint16) + } + + return uint16(varInt), nil +} + +// tlvNumBytes takes the length of the payload and returns the number of bytes +// that it would take to serialise such a payload. For the TLV type encoding, +// the payload length itself would be encoded as a var-int, this is then +// followed by the payload itself and finally an HMAC would be appended. +func tlvNumBytes(payloadLen int) int { + return wire.VarIntSerializeSize(uint64(payloadLen)) + payloadLen + + HMACSize +} + +// encodeTLVHopPayload takes a HopPayload and writes it to the given writer +// using the TLV encoding which requires the payload and HMAC to be pre-fixed +// with a var-int encoded length. +func encodeTLVHopPayload(hp *HopPayload, w io.Writer) error { + // First, the length of the payload is encoded as a var-int. + var b [8]byte + err := WriteVarInt(w, uint64(len(hp.Payload)), &b) + if err != nil { + return err + } + + // Then, the raw payload and he HMAC are written in series. + if _, err := w.Write(hp.Payload); err != nil { + return err + } + + _, err = w.Write(hp.HMAC[:]) + + return err +} + +// HopData is the information destined for individual hops. It is a fixed size +// 64 bytes, prefixed with a 1 byte realm that indicates how to interpret it. +// For now we simply assume it's the bitcoin realm (0x00) and hence the format +// is fixed. The last 32 bytes are always the HMAC to be passed to the next +// hop, or zero if this is the packet is not to be forwarded, since this is the +// last hop. +type HopData struct { + // Realm denotes the "real" of target chain of the next hop. For + // bitcoin, this value will be 0x00. + Realm [RealmByteSize]byte + + // NextAddress is the address of the next hop that this packet should + // be forward to. + NextAddress [AddressSize]byte + + // ForwardAmount is the HTLC amount that the next hop should forward. + // This value should take into account the fee require by this + // particular hop, and the cumulative fee for the entire route. + ForwardAmount uint64 + + // OutgoingCltv is the value of the outgoing absolute time-lock that + // should be included in the HTLC forwarded. + OutgoingCltv uint32 + + // ExtraBytes is the set of unused bytes within the onion payload. This + // extra set of bytes can be utilized by higher level applications to + // package additional data within the per-hop payload, or signal that a + // portion of the remaining set of hops are to be consumed as Extra + // Onion Blobs. + // + // TODO(roasbeef): rename to padding bytes? + ExtraBytes [NumPaddingBytes]byte +} + +// Encode writes the serialized version of the target HopData into the passed +// io.Writer. +func (hd *HopData) Encode(w io.Writer) error { + if _, err := w.Write(hd.Realm[:]); err != nil { + return err + } + + if _, err := w.Write(hd.NextAddress[:]); err != nil { + return err + } + + err := binary.Write(w, binary.BigEndian, hd.ForwardAmount) + if err != nil { + return err + } + + err = binary.Write(w, binary.BigEndian, hd.OutgoingCltv) + if err != nil { + return err + } + + if _, err := w.Write(hd.ExtraBytes[:]); err != nil { + return err + } + + return nil +} + +// Decode Decodes populates the target HopData with the contents of a serialized +// HopData packed into the passed io.Reader. +func (hd *HopData) Decode(r io.Reader) error { + if _, err := io.ReadFull(r, hd.Realm[:]); err != nil { + return err + } + + if _, err := io.ReadFull(r, hd.NextAddress[:]); err != nil { + return err + } + + err := binary.Read(r, binary.BigEndian, &hd.ForwardAmount) + if err != nil { + return err + } + + err = binary.Read(r, binary.BigEndian, &hd.OutgoingCltv) + if err != nil { + return err + } + + _, err = io.ReadFull(r, hd.ExtraBytes[:]) + return err +} + +// NewLegacyHopPayload creates a new hop payload given a set of forwarding +// instructions specified as HopData for a hop. This is the legacy encoding +// for a HopPayload. +func NewLegacyHopPayload(hopData *HopData) (HopPayload, error) { + var ( + h HopPayload + b bytes.Buffer + ) + + if err := hopData.Encode(&b); err != nil { + return h, nil + } + + // We'll also mark that this particular hop will be using the legacy + // format as the modern format packs the existing hop data information + // into the EOB space as a TLV stream. + h.Type = PayloadLegacy + h.Payload = b.Bytes() + + return h, nil +} + +// legacyPayloadSize returns the size of payloads encoded using the legacy +// fixed-size encoding. +func legacyPayloadSize() uint16 { + return LegacyHopDataSize - HMACSize +} + +// legacyNumBytes returns the number of bytes it will take to serialize the full +// payload. For the legacy encoding type, this is always a fixed number. +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. +func isLegacyPayloadByte(b byte) bool { + return b == 0x00 +} + +// encodeLegacyHopPayload takes a HopPayload and writes it to the given writer +// using the legacy encoding. +func encodeLegacyHopPayload(hp *HopPayload, w io.Writer) error { + // The raw payload and he HMAC are written in series. + if _, err := w.Write(hp.Payload); err != nil { + return err + } + + _, err := w.Write(hp.HMAC[:]) + + return err +} + +// decodeLegacyHopData takes a payload and decodes it into a HopData struct. +func decodeLegacyHopData(payload []byte) (*HopData, error) { + var ( + payloadReader = bytes.NewBuffer(payload) + hd HopData + ) + if err := hd.Decode(payloadReader); err != nil { + return nil, err + } + + return &hd, nil +} diff --git a/sphinx.go b/sphinx.go index 36e9a81..501c061 100644 --- a/sphinx.go +++ b/sphinx.go @@ -521,20 +521,48 @@ func (r *Router) Stop() { r.log.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 +} + +// ProcessOnionOpt defines the signature of a function option that can be used +// to modify how an onion is processed. +type ProcessOnionOpt func(cfg *processOnionCfg) + +// WithBlindingPoint is a function option that can be used to set the blinding +// point to be used when processing an onion. +func WithBlindingPoint(point *btcec.PublicKey) ProcessOnionOpt { + return func(cfg *processOnionCfg) { + cfg.blindingPoint = point + } +} + // 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 -// derived shared secret has been seen before the packet is rejected. Finally -// if the MAC doesn't check the packet is again rejected. +// derived shared secret has been seen before the packet is rejected. If the +// blinded point is specified, then it will be used along with the ephemeral key +// in the onion packet to derive the shared secret. Finally, if the MAC doesn't +// check the packet is again rejected. // // In the case of a successful packet processing, and ProcessedPacket struct is // returned which houses the newly parsed packet, along with instructions on // what to do next. -func (r *Router) ProcessOnionPacket(onionPkt *OnionPacket, - assocData []byte, incomingCltv uint32) (*ProcessedPacket, error) { +func (r *Router) ProcessOnionPacket(onionPkt *OnionPacket, assocData []byte, + incomingCltv uint32, opts ...ProcessOnionOpt) (*ProcessedPacket, + error) { + + cfg := &processOnionCfg{} + for _, o := range opts { + o(cfg) + } // Compute the shared secret for this onion packet. - sharedSecret, err := r.generateSharedSecret(onionPkt.EphemeralKey) + sharedSecret, err := r.generateSharedSecret( + onionPkt.EphemeralKey, cfg.blindingPoint, + ) if err != nil { return nil, err } @@ -546,7 +574,7 @@ func (r *Router) ProcessOnionPacket(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, r) + packet, err := processOnionPacket(onionPkt, &sharedSecret, assocData) if err != nil { return nil, err } @@ -564,16 +592,39 @@ func (r *Router) ProcessOnionPacket(onionPkt *OnionPacket, // // NOTE: This method does not do any sort of replay protection, and should only // be used to reconstruct packets that were successfully processed previously. -func (r *Router) ReconstructOnionPacket(onionPkt *OnionPacket, - assocData []byte) (*ProcessedPacket, error) { +func (r *Router) ReconstructOnionPacket(onionPkt *OnionPacket, assocData []byte, + opts ...ProcessOnionOpt) (*ProcessedPacket, error) { + + cfg := &processOnionCfg{} + for _, o := range opts { + o(cfg) + } // Compute the shared secret for this onion packet. - sharedSecret, err := r.generateSharedSecret(onionPkt.EphemeralKey) + sharedSecret, err := r.generateSharedSecret( + onionPkt.EphemeralKey, cfg.blindingPoint, + ) if err != nil { return nil, err } - return processOnionPacket(onionPkt, &sharedSecret, assocData, r) + return processOnionPacket(onionPkt, &sharedSecret, assocData) +} + +// DecryptBlindedHopData uses the router's private key to decrypt data encrypted +// by the creator of the blinded route. +func (r *Router) DecryptBlindedHopData(ephemPub *btcec.PublicKey, + encryptedData []byte) ([]byte, error) { + + return decryptBlindedHopData(r.onionKey, ephemPub, encryptedData) +} + +// NextEphemeral computes the next ephemeral key given the current ephemeral +// key and the router's private key. +func (r *Router) NextEphemeral(ephemPub *btcec.PublicKey) (*btcec.PublicKey, + error) { + + return NextEphemeral(r.onionKey, ephemPub) } // unwrapPacket wraps a layer of the passed onion packet using the specified @@ -640,8 +691,7 @@ func unwrapPacket(onionPkt *OnionPacket, sharedSecret *Hash256, // 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, - sharedSecretGen sharedSecretGenerator) (*ProcessedPacket, error) { + assocData []byte) (*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 @@ -721,18 +771,25 @@ func (r *Router) BeginTxn(id []byte, nels int) *Tx { // 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 -// derived shared secret has been seen before the packet is rejected. Finally -// if the MAC doesn't check the packet is again rejected. +// derived shared secret has been seen before the packet is rejected. If the +// blinded point is specified, then it will be used along with the ephemeral key +// in the onion packet to derive the shared secret. Finally, if the MAC doesn't +// check the packet is again rejected. // // In the case of a successful packet processing, and ProcessedPacket struct is // returned which houses the newly parsed packet, along with instructions on // what to do next. func (t *Tx) ProcessOnionPacket(seqNum uint16, onionPkt *OnionPacket, - assocData []byte, incomingCltv uint32) error { + assocData []byte, incomingCltv uint32, opts ...ProcessOnionOpt) error { + + cfg := &processOnionCfg{} + for _, o := range opts { + o(cfg) + } // Compute the shared secret for this onion packet. sharedSecret, err := t.router.generateSharedSecret( - onionPkt.EphemeralKey, + onionPkt.EphemeralKey, cfg.blindingPoint, ) if err != nil { return err @@ -745,9 +802,7 @@ 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, t.router, - ) + packet, err := processOnionPacket(onionPkt, &sharedSecret, assocData) if err != nil { return err } diff --git a/sphinx_test.go b/sphinx_test.go index 3425e76..7d99ba3 100644 --- a/sphinx_test.go +++ b/sphinx_test.go @@ -5,13 +5,15 @@ import ( "encoding/hex" "encoding/json" "fmt" - "io/ioutil" + "io" + "os" "reflect" "testing" "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/chaincfg" "github.com/davecgh/go-spew/spew" + "github.com/stretchr/testify/require" ) // BOLT 4 Test Vectors @@ -66,7 +68,7 @@ func newTestRoute(numHops int) ([]*Router, *PaymentPath, *[]HopData, *OnionPacke } copy(hopData.NextAddress[:], bytes.Repeat([]byte{byte(i)}, 8)) - hopPayload, err := NewHopPayload(&hopData, nil) + hopPayload, err := NewLegacyHopPayload(&hopData) if err != nil { return nil, nil, nil, nil, fmt.Errorf("unable to "+ "create new hop payload: %v", err) @@ -127,7 +129,7 @@ func TestBolt4Packet(t *testing.T) { copy(hopData.NextAddress[:], bytes.Repeat([]byte{byte(i)}, 8)) hopsData = append(hopsData, hopData) - hopPayload, err := NewHopPayload(&hopData, nil) + hopPayload, err := NewLegacyHopPayload(&hopData) if err != nil { t.Fatalf("unable to make hop payload: %v", err) } @@ -180,7 +182,9 @@ func TestSphinxCorrectness(t *testing.T) { hop := nodes[i] t.Logf("Processing at hop: %v \n", i) - onionPacket, err := hop.ProcessOnionPacket(fwdMsg, nil, uint32(i)+1) + onionPacket, err := hop.ProcessOnionPacket( + fwdMsg, nil, uint32(i)+1, + ) if err != nil { t.Fatalf("Node %v was unable to process the "+ "forwarding message: %v", i, err) @@ -269,14 +273,17 @@ func TestSphinxNodeRelpay(t *testing.T) { // Allow the node to process the initial packet, this should proceed // without any failures. - if _, err := nodes[0].ProcessOnionPacket(fwdMsg, nil, 1); err != nil { + _, err = nodes[0].ProcessOnionPacket(fwdMsg, nil, 1) + if err != nil { t.Fatalf("unable to process sphinx packet: %v", err) } // Now, force the node to process the packet a second time, this should // fail with a detected replay error. - if _, err := nodes[0].ProcessOnionPacket(fwdMsg, nil, 1); err != ErrReplayedPacket { - t.Fatalf("sphinx packet replay should be rejected, instead error is %v", err) + _, err = nodes[0].ProcessOnionPacket(fwdMsg, nil, 1) + if err != ErrReplayedPacket { + t.Fatalf("sphinx packet replay should be rejected, instead "+ + "error is %v", err) } } @@ -296,14 +303,14 @@ func TestSphinxNodeRelpaySameBatch(t *testing.T) { // Allow the node to process the initial packet, this should proceed // without any failures. - if err := tx.ProcessOnionPacket(0, fwdMsg, nil, 1); err != nil { + if err := tx.ProcessOnionPacket(0, fwdMsg, nil, 1, nil); err != nil { t.Fatalf("unable to process sphinx packet: %v", err) } // Now, force the node to process the packet a second time, this call // should not fail, even though the batch has internally recorded this // as a duplicate. - err = tx.ProcessOnionPacket(1, fwdMsg, nil, 1) + err = tx.ProcessOnionPacket(1, fwdMsg, nil, 1, nil) if err != nil { t.Fatalf("adding duplicate sphinx packet to batch should not "+ "result in an error, instead got: %v", err) @@ -342,7 +349,8 @@ func TestSphinxNodeRelpayLaterBatch(t *testing.T) { // Allow the node to process the initial packet, this should proceed // without any failures. - if err := tx.ProcessOnionPacket(uint16(0), fwdMsg, nil, 1); err != nil { + err = tx.ProcessOnionPacket(uint16(0), fwdMsg, nil, 1, nil) + if err != nil { t.Fatalf("unable to process sphinx packet: %v", err) } @@ -355,7 +363,7 @@ func TestSphinxNodeRelpayLaterBatch(t *testing.T) { // Now, force the node to process the packet a second time, this should // fail with a detected replay error. - err = tx2.ProcessOnionPacket(uint16(0), fwdMsg, nil, 1) + err = tx2.ProcessOnionPacket(uint16(0), fwdMsg, nil, 1, nil) if err != nil { t.Fatalf("sphinx packet replay should not have been rejected, "+ "instead error is %v", err) @@ -387,7 +395,8 @@ func TestSphinxNodeReplayBatchIdempotency(t *testing.T) { // Allow the node to process the initial packet, this should proceed // without any failures. - if err := tx.ProcessOnionPacket(uint16(0), fwdMsg, nil, 1); err != nil { + err = tx.ProcessOnionPacket(uint16(0), fwdMsg, nil, 1, nil) + if err != nil { t.Fatalf("unable to process sphinx packet: %v", err) } @@ -400,7 +409,7 @@ func TestSphinxNodeReplayBatchIdempotency(t *testing.T) { // Now, force the node to process the packet a second time, this should // not fail with a detected replay error. - err = tx2.ProcessOnionPacket(uint16(0), fwdMsg, nil, 1) + err = tx2.ProcessOnionPacket(uint16(0), fwdMsg, nil, 1, nil) if err != nil { t.Fatalf("sphinx packet replay should not have been rejected, "+ "instead error is %v", err) @@ -521,8 +530,8 @@ func newEOBRoute(numHops uint32, return fwdMsg, nodes, nil } -func mustNewHopPayload(hopData *HopData, eob []byte) HopPayload { - payload, err := NewHopPayload(hopData, eob) +func mustNewLegacyHopPayload(hopData *HopData) HopPayload { + payload, err := NewLegacyHopPayload(hopData) if err != nil { panic(err) } @@ -575,12 +584,12 @@ func TestSphinxHopVariableSizedPayloads(t *testing.T) { { numNodes: 2, eobMapping: map[int]HopPayload{ - 0: mustNewHopPayload(&HopData{ + 0: mustNewLegacyHopPayload(&HopData{ Realm: [1]byte{0x00}, ForwardAmount: 2, OutgoingCltv: 3, NextAddress: [8]byte{1, 1, 1, 1, 1, 1, 1, 1}, - }, nil), + }), 1: HopPayload{ Type: PayloadTLV, Payload: bytes.Repeat([]byte("a"), LegacyHopDataSize*2), @@ -598,12 +607,12 @@ func TestSphinxHopVariableSizedPayloads(t *testing.T) { Type: PayloadTLV, Payload: bytes.Repeat([]byte("a"), 100), }, - 1: mustNewHopPayload(&HopData{ + 1: mustNewLegacyHopPayload(&HopData{ Realm: [1]byte{0x00}, ForwardAmount: 22, OutgoingCltv: 9, NextAddress: [8]byte{1, 1, 1, 1, 1, 1, 1, 1}, - }, nil), + }), 2: HopPayload{ Type: PayloadTLV, Payload: bytes.Repeat([]byte("a"), 256), @@ -749,8 +758,14 @@ func TestSphinxHopVariableSizedPayloads(t *testing.T) { } } -// testFileName is the name of the multi-frame onion test file. -const testFileName = "testdata/onion-test-multi-frame.json" +const ( + // testMultiFrameFileName is the name of the multi-frame onion test + // file. + testMultiFrameFileName = "testdata/onion-test-multi-frame.json" + + // testTLVFileName is the name of the tlv-payload-only onion test file. + testTLVFileName = "testdata/onion-test.json" +) type jsonHop struct { Type string `json:"type"` @@ -795,6 +810,85 @@ func jsonTypeToPayloadType(jsonType string) PayloadType { } } +// TestTLVPayloadOnion tests the construction of an onion where all the payloads +// are of the TLV type. +func TestTLVPayloadOnion(t *testing.T) { + t.Parallel() + + // First, we'll read out the raw JSON file at the target location. + jsonBytes, err := os.ReadFile(testTLVFileName) + require.NoError(t, err) + + // Once we have the raw file, we'll unpack it into our jsonTestCase + // struct defined above. + testCase := &jsonTestCase{} + 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.Generate.Hops { + pubKeyBytes, err := hex.DecodeString(hop.Pubkey) + require.NoError(t, err) + + pubKey, err := btcec.ParsePubKey(pubKeyBytes) + require.NoError(t, err) + + // The test has already encoded the length of the TLV payloads, + // so to make it compatible with our PaymentPath, we just + // extract the payload. + payload, err := hex.DecodeString(hop.Payload) + require.NoError(t, err) + + var ( + bufReader = bytes.NewReader(payload) + b [8]byte + ) + varInt, err := ReadVarInt(bufReader, &b) + require.NoError(t, err) + + payloadSize := uint32(varInt) + require.NoError(t, err) + + hopPayload := make([]byte, payloadSize) + _, err = io.ReadFull(bufReader, hopPayload) + require.NoError(t, err) + + route[i] = OnionHop{ + NodePub: *pubKey, + HopPayload: HopPayload{ + Type: PayloadTLV, + Payload: hopPayload, + }, + } + } + + finalPacket, err := hex.DecodeString(testCase.Onion) + require.NoError(t, err) + + sessionKeyBytes, err := hex.DecodeString(testCase.Generate.SessionKey) + require.NoError(t, err) + + assocData, err := hex.DecodeString(testCase.Generate.AssociatedData) + 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, assocData, 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, b.Bytes(), finalPacket, "final packet does not "+ + "match expected BOLT 4 packet, want: %s, got %s", + hex.EncodeToString(finalPacket), hex.EncodeToString(b.Bytes())) +} + // TestVariablePayloadOnion tests that if we construct a packet that contains a // mix of the old and new payload format, that we match the version that's // included in the spec. @@ -802,35 +896,26 @@ func TestVariablePayloadOnion(t *testing.T) { t.Parallel() // First, we'll read out the raw JSOn file at the target location. - jsonBytes, err := ioutil.ReadFile(testFileName) - if err != nil { - t.Fatalf("unable to read json file: %v", err) - } + jsonBytes, err := os.ReadFile(testMultiFrameFileName) + require.NoError(t, err) // Once we have the raw file, we'll unpack it into our jsonTestCase // struct defined above. testCase := &jsonTestCase{} - if err := json.Unmarshal(jsonBytes, testCase); err != nil { - t.Fatalf("unable to parse spec json file: %v", err) - } + 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.Generate.Hops { pubKeyBytes, err := hex.DecodeString(hop.Pubkey) - if err != nil { - t.Fatalf("unable to decode pubkey: %v", err) - } + require.NoError(t, err) + pubKey, err := btcec.ParsePubKey(pubKeyBytes) - if err != nil { - t.Fatalf("unable to parse BOLT 4 pubkey #%d: %v", i, err) - } + require.NoError(t, err) payload, err := hex.DecodeString(hop.Payload) - if err != nil { - t.Fatalf("unable to decode payload: %v", err) - } + require.NoError(t, err) payloadType := jsonTypeToPayloadType(hop.Type) route[i] = OnionHop{ @@ -854,39 +939,27 @@ func TestVariablePayloadOnion(t *testing.T) { } finalPacket, err := hex.DecodeString(testCase.Onion) - if err != nil { - t.Fatalf("unable to decode packet: %v", err) - } + require.NoError(t, err) sessionKeyBytes, err := hex.DecodeString(testCase.Generate.SessionKey) - if err != nil { - t.Fatalf("unable to generate session key: %v", err) - } + require.NoError(t, err) - associatedData, err := hex.DecodeString(testCase.Generate.AssociatedData) - if err != nil { - t.Fatalf("unable to decode AD: %v", err) - } + assocData, err := hex.DecodeString(testCase.Generate.AssociatedData) + 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, associatedData, DeterministicPacketFiller, + &route, sessionKey, assocData, DeterministicPacketFiller, ) - if err != nil { - t.Fatalf("unable to construct onion packet: %v", err) - } + require.NoError(t, err) var b bytes.Buffer - if err := pkt.Encode(&b); err != nil { - t.Fatalf("unable to decode onion packet: %v", err) - } + require.NoError(t, pkt.Encode(&b)) // Finally, we expect that our packet matches the packet included in // the spec's test vectors. - if bytes.Compare(b.Bytes(), finalPacket) != 0 { - t.Fatalf("final packet does not match expected BOLT 4 packet, "+ - "want: %s, got %s", hex.EncodeToString(finalPacket), - hex.EncodeToString(b.Bytes())) - } + require.Equalf(t, b.Bytes(), finalPacket, "final packet does not "+ + "match expected BOLT 4 packet, want: %s, got %s", + hex.EncodeToString(finalPacket), hex.EncodeToString(b.Bytes())) } diff --git a/testdata/onion-route-blinding-test.json b/testdata/onion-route-blinding-test.json new file mode 100644 index 0000000..93cbba2 --- /dev/null +++ b/testdata/onion-route-blinding-test.json @@ -0,0 +1,183 @@ +{ + "comment": "test vector for a payment onion sent to a partially blinded route", + "generate": { + "comment": "This section contains test data for creating a payment onion that sends to the provided blinded route.", + "session_key": "0303030303030303030303030303030303030303030303030303030303030303", + "associated_data": "4242424242424242424242424242424242424242424242424242424242424242", + "final_amount_msat": 100000, + "final_cltv": 749000, + "blinded_payinfo": { + "comment": "total costs for using the blinded path", + "fee_base_msat": 10100, + "fee_proportional_millionths": 251, + "cltv_expiry_delta": 150 + }, + "blinded_route": { + "comment": "This section contains a blinded route that the sender will use for his payment, usually obtained from a Bolt 12 invoice.", + "introduction_node_id": "0324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c", + "blinding": "024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d0766", + "hops": [ + { + "alias": "Bob", + "blinded_node_id": "03da173ad2aee2f701f17e59fbd16cb708906d69838a5f088e8123fb36e89a2c25", + "encrypted_data": "cd7b00ff9c09ed28102b210ac73aa12d63e90852cebc496c49f57c499a2888b49f2e72b19446f7e60a818aa2938d8c625415b992b8928a7321edb8f7cea40de362bed082ad51acc6156dca5532fb68" + }, + { + "alias": "Carol", + "blinded_node_id": "02e466727716f044290abf91a14a6d90e87487da160c2a3cbd0d465d7a78eb83a7", + "encrypted_data": "cc0f16524fd7f8bb0f4e8d40ad71709ef140174c76faa574cac401bb8992fef76c4d004aa485dd599ed1cf2715f570f656a5aaecaf1ee8dc9d0fa1d424759be1932a8f29fac08bc2d2a1ed7159f28b" + }, + { + "alias": "Dave", + "blinded_node_id": "036861b366f284f0a11738ffbf7eda46241a8977592878fe3175ae1d1e4754eccf", + "encrypted_data": "0fa1a72cff3b64a3d6e1e4903cf8c8b0a17144aeb249dcb86561adee1f679ee8db3e561d9e49895fd4bcebf6f58d6f61a6d41a9bf5aa4b0453437856632e8255c351873143ddf2bb2b0832b091e1b4" + }, + { + "alias": "Eve", + "blinded_node_id": "021982a48086cb8984427d3727fe35a03d396b234f0701f5249daa12e8105c8dae", + "encrypted_data": "da1c7e5f7881219884beae6ae68971de73bab4c3055d9865b1afb60722a63c688768042ade22f2c22f5724767d171fd221d3e579e43b354cc72e3ef146ada91a892d95fc48662f5b158add0af457da" + } + ] + }, + "full_route": { + "comment": "The sender adds one normal hop through Alice, who doesn't support blinded payments (and doesn't charge a fee). The sender provides the initial blinding point in Bob's onion payload, and encrypted_data for each node in the blinded route.", + "hops": [ + { + "alias": "Alice", + "pubkey": "02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619", + "payload": "14020301ae2d04030b6e5e0608000000000000000a", + "tlvs": { + "outgoing_channel_id": "0x0x10", + "amt_to_forward": 110125, + "outgoing_cltv_value": 749150 + } + }, + { + "alias": "Bob", + "pubkey": "0324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c", + "payload": "740a4fcd7b00ff9c09ed28102b210ac73aa12d63e90852cebc496c49f57c499a2888b49f2e72b19446f7e60a818aa2938d8c625415b992b8928a7321edb8f7cea40de362bed082ad51acc6156dca5532fb680c21024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d0766", + "tlvs": { + "current_blinding_point": "024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d0766", + "encrypted_recipient_data": { + "padding": "0000000000000000000000000000000000000000000000000000000000000000", + "short_channel_id": "0x0x1", + "payment_relay": { + "cltv_expiry_delta": 50, + "fee_proportional_millionths": 0, + "fee_base_msat": 10000 + }, + "payment_constraints": { + "max_cltv_expiry": 750150, + "htlc_minimum_msat": 50 + }, + "allowed_features": { + "features": [] + } + } + } + }, + { + "alias": "Carol", + "pubkey": "02e466727716f044290abf91a14a6d90e87487da160c2a3cbd0d465d7a78eb83a7", + "payload": "510a4fcc0f16524fd7f8bb0f4e8d40ad71709ef140174c76faa574cac401bb8992fef76c4d004aa485dd599ed1cf2715f570f656a5aaecaf1ee8dc9d0fa1d424759be1932a8f29fac08bc2d2a1ed7159f28b", + "tlvs": { + "encrypted_recipient_data": { + "short_channel_id": "0x0x2", + "next_blinding_override": "031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f", + "payment_relay": { + "cltv_expiry_delta": 75, + "fee_proportional_millionths": 150, + "fee_base_msat": 100 + }, + "payment_constraints": { + "max_cltv_expiry": 750100, + "htlc_minimum_msat": 50 + }, + "allowed_features": { + "features": [] + } + } + } + }, + { + "alias": "Dave", + "pubkey": "036861b366f284f0a11738ffbf7eda46241a8977592878fe3175ae1d1e4754eccf", + "payload": "510a4f0fa1a72cff3b64a3d6e1e4903cf8c8b0a17144aeb249dcb86561adee1f679ee8db3e561d9e49895fd4bcebf6f58d6f61a6d41a9bf5aa4b0453437856632e8255c351873143ddf2bb2b0832b091e1b4", + "tlvs": { + "encrypted_recipient_data": { + "padding": "00000000000000000000000000000000000000000000000000000000000000000000", + "short_channel_id": "0x0x3", + "payment_relay": { + "cltv_expiry_delta": 25, + "fee_proportional_millionths": 100 + }, + "payment_constraints": { + "max_cltv_expiry": 750025, + "htlc_minimum_msat": 50 + }, + "allowed_features": { + "features": [] + } + } + } + }, + { + "alias": "Eve", + "pubkey": "021982a48086cb8984427d3727fe35a03d396b234f0701f5249daa12e8105c8dae", + "payload": "6002030186a004030b6dc80a4fda1c7e5f7881219884beae6ae68971de73bab4c3055d9865b1afb60722a63c688768042ade22f2c22f5724767d171fd221d3e579e43b354cc72e3ef146ada91a892d95fc48662f5b158add0af457da12030249f0", + "tlvs": { + "amt_to_forward": 100000, + "total_amount_msat": 150000, + "outgoing_cltv_value": 749000, + "encrypted_recipient_data": { + "padding": "00000000000000000000000000000000000000000000000000000000", + "path_id": "c9cf92f45ade68345bc20ae672e2012f4af487ed4415", + "payment_constraints": { + "max_cltv_expiry": 750000, + "htlc_minimum_msat": 50 + }, + "allowed_features": { + "features": [] + } + } + } + } + ] + }, + "onion": "0002531fe6068134503d2723133227c867ac8fa6c83c537e9a44c3c5bdbdcb1fe337dadf610256c6ab518495dce9cdedf9391e21a71dada75be905267ba82f326c0513dda706908cfee834996700f881b2aed106585d61a2690de4ebe5d56ad2013b520af2a3c49316bc590ee83e8c31b1eb11ff766dad27ca993326b1ed582fb451a2ad87fbf6601134c6341c4a2deb6850e25a355be68dbb6923dc89444fdd74a0f700433b667bda345926099f5547b07e97ad903e8a01566a78ae177366239e793dac719de805565b6d0a1d290e273f705cfc56873f8b5e28225f7ded7a1d4ceffae63f91e477be8c917c786435976102a924ba4ba3de6150c829ce01c25428f2f5d05ef023be7d590ecdf6603730db3948f80ca1ed3d85227e64ef77200b9b557f427b6e1073cfa0e63e4485441768b98ab11ba8104a6cee1d7af7bb5ee9c05cf9cf4718901e92e09dfe5cb3af336a953072391c1e91fc2f4b92e124b38e0c6d17ef6ba7bbe93f02046975bb01b7f766fcfc5a755af11a90cc7eb3505986b56e07a7855534d03b79f0dfbfe645b0d6d4185c038771fd25b800aa26b2ed2e30b1e713659468618a2fea04fcd0473284598f76b11b0d159d343bc9711d3bea8d561547bcc8fff12317c0e7b1ee75bcb8082d762b6417f99d0f71ff7c060f6b564ad6827edaffa72eefcc4ce633a8da8d41c19d8f6aebd8878869eb518ccc16dccae6a94c690957598ce0295c1c46af5d7a2f0955b5400526bfd1430f554562614b5d00feff3946427be520dee629b76b6a9c2b1da6701c8ca628a69d6d40e20dd69d6e879d7a052d9c16f544b49738c7ff3cdd0613e9ed00ead7707702d1a6a0b88de1927a50c36beb78f4ff81e3dd97b706307596eebb363d418a891e1cb4589ce86ce81cdc0e1473d7a7dd5f6bb6e147c1f7c46fa879b4512c25704da6cdbb3c123a72e3585dc07b3e5cbe7fecf3a08426eee8c70ddc46ebf98b0bcb14a08c469cb5cfb6702acc0befd17640fa60244eca491280a95fbbc5833d26e4be70fcf798b55e06eb9fcb156942dcf108236f32a5a6c605687ba4f037eddbb1834dcbcd5293a0b66c621346ca5d893d239c26619b24c71f25cecc275e1ab24436ac01c80c0006fab2d95e82e3a0c3ea02d08ec5b24eb39205c49f4b549dcab7a88962336c4624716902f4e08f2b23cfd324f18405d66e9da3627ac34a6873ba2238386313af20d5a13bbd507fdc73015a17e3bd38fae1145f7f70d7cb8c5e1cdf9cf06d1246592a25d56ec2ae44cd7f75aa7f5f4a2b2ee49a41a26be4fab3f3f2ceb7b08510c5e2b7255326e4c417325b333cafe96dde1314a15dd6779a7d5a8a40622260041e936247eec8ec39ca29a1e18161db37497bdd4447a7d5ef3b8d22a2acd7f486b152bb66d3a15afc41dc9245a8d75e1d33704d4471e417ccc8d31645fdd647a2c191692675cf97664951d6ce98237d78b0962ad1433b5a3e49ddddbf57a391b14dcce00b4d7efe5cbb1e78f30d5ef53d66c381a45e275d2dcf6be559acb3c42494a9a2156eb8dcf03dd92b2ebaa697ea628fa0f75f125e4a7daa10f8dcf56ebaf7814557708c75580fad2bbb33e66ad7a4788a7aaac792aaae76138d7ff09df6a1a1920ddcf22e5e7007b15171b51ff81799355232ce39f7d5ceeaf704255d790041d6390a69f42816cba641ec81faa3d7c0fdec59dfe4ca41f31a692eaffc66b083995d86c575aea4514a3e09e8b3a1fa4d1591a2505f253ad0b6bfd9d87f063d2be414d3a427c0506a88ac5bdbef9b50d73bce876f85c196dca435e210e1d6713695b529ddda3350fb5065a6a8288abd265380917bac8ebbc7d5ced564587471dddf90c22ce6dbadea7e7a6723438d4cf6ac6dae27d033a8cadd77ab262e8defb33445ddb2056ec364c7629c33745e2338" + }, + "decrypt": { + "comment": "This section contains the internal values generated by intermediate nodes when decrypting their payload.", + "hops": [ + { + "alias": "Alice", + "onion": "0002531fe6068134503d2723133227c867ac8fa6c83c537e9a44c3c5bdbdcb1fe337dadf610256c6ab518495dce9cdedf9391e21a71dada75be905267ba82f326c0513dda706908cfee834996700f881b2aed106585d61a2690de4ebe5d56ad2013b520af2a3c49316bc590ee83e8c31b1eb11ff766dad27ca993326b1ed582fb451a2ad87fbf6601134c6341c4a2deb6850e25a355be68dbb6923dc89444fdd74a0f700433b667bda345926099f5547b07e97ad903e8a01566a78ae177366239e793dac719de805565b6d0a1d290e273f705cfc56873f8b5e28225f7ded7a1d4ceffae63f91e477be8c917c786435976102a924ba4ba3de6150c829ce01c25428f2f5d05ef023be7d590ecdf6603730db3948f80ca1ed3d85227e64ef77200b9b557f427b6e1073cfa0e63e4485441768b98ab11ba8104a6cee1d7af7bb5ee9c05cf9cf4718901e92e09dfe5cb3af336a953072391c1e91fc2f4b92e124b38e0c6d17ef6ba7bbe93f02046975bb01b7f766fcfc5a755af11a90cc7eb3505986b56e07a7855534d03b79f0dfbfe645b0d6d4185c038771fd25b800aa26b2ed2e30b1e713659468618a2fea04fcd0473284598f76b11b0d159d343bc9711d3bea8d561547bcc8fff12317c0e7b1ee75bcb8082d762b6417f99d0f71ff7c060f6b564ad6827edaffa72eefcc4ce633a8da8d41c19d8f6aebd8878869eb518ccc16dccae6a94c690957598ce0295c1c46af5d7a2f0955b5400526bfd1430f554562614b5d00feff3946427be520dee629b76b6a9c2b1da6701c8ca628a69d6d40e20dd69d6e879d7a052d9c16f544b49738c7ff3cdd0613e9ed00ead7707702d1a6a0b88de1927a50c36beb78f4ff81e3dd97b706307596eebb363d418a891e1cb4589ce86ce81cdc0e1473d7a7dd5f6bb6e147c1f7c46fa879b4512c25704da6cdbb3c123a72e3585dc07b3e5cbe7fecf3a08426eee8c70ddc46ebf98b0bcb14a08c469cb5cfb6702acc0befd17640fa60244eca491280a95fbbc5833d26e4be70fcf798b55e06eb9fcb156942dcf108236f32a5a6c605687ba4f037eddbb1834dcbcd5293a0b66c621346ca5d893d239c26619b24c71f25cecc275e1ab24436ac01c80c0006fab2d95e82e3a0c3ea02d08ec5b24eb39205c49f4b549dcab7a88962336c4624716902f4e08f2b23cfd324f18405d66e9da3627ac34a6873ba2238386313af20d5a13bbd507fdc73015a17e3bd38fae1145f7f70d7cb8c5e1cdf9cf06d1246592a25d56ec2ae44cd7f75aa7f5f4a2b2ee49a41a26be4fab3f3f2ceb7b08510c5e2b7255326e4c417325b333cafe96dde1314a15dd6779a7d5a8a40622260041e936247eec8ec39ca29a1e18161db37497bdd4447a7d5ef3b8d22a2acd7f486b152bb66d3a15afc41dc9245a8d75e1d33704d4471e417ccc8d31645fdd647a2c191692675cf97664951d6ce98237d78b0962ad1433b5a3e49ddddbf57a391b14dcce00b4d7efe5cbb1e78f30d5ef53d66c381a45e275d2dcf6be559acb3c42494a9a2156eb8dcf03dd92b2ebaa697ea628fa0f75f125e4a7daa10f8dcf56ebaf7814557708c75580fad2bbb33e66ad7a4788a7aaac792aaae76138d7ff09df6a1a1920ddcf22e5e7007b15171b51ff81799355232ce39f7d5ceeaf704255d790041d6390a69f42816cba641ec81faa3d7c0fdec59dfe4ca41f31a692eaffc66b083995d86c575aea4514a3e09e8b3a1fa4d1591a2505f253ad0b6bfd9d87f063d2be414d3a427c0506a88ac5bdbef9b50d73bce876f85c196dca435e210e1d6713695b529ddda3350fb5065a6a8288abd265380917bac8ebbc7d5ced564587471dddf90c22ce6dbadea7e7a6723438d4cf6ac6dae27d033a8cadd77ab262e8defb33445ddb2056ec364c7629c33745e2338", + "node_privkey": "4141414141414141414141414141414141414141414141414141414141414141" + }, + { + "alias": "Bob", + "onion": "000280caa47c2a0ea677f6a77529e46caa04212153a8d5f829bee1e7339b17e2e2a9a3461d10472364a4ff12344beb6df96fb0c38ec47d1e956ddff5a665190fcca5ed02c3a3903fd8bbd4a4b95b197867c378b67b08f0624cfe80734ba512869c0fa22099beb1f6f1ea325b07ce7449736d7ffad79178b428d8ea2d7bc6578f12dbd788ef933f3b5ba352797c41f6786c3820c96726acf8bddf2cfa5d9c617d2b0bd5ab7b93f7964c98f44cf47db8422f47d11100236a29579f1cafcd38bd979814e1d2bf6d625edf50e1e21bfaf6268e3180dd7aafd3892da281c6dd53c1c366d0fdaf670b6ad84a38d6e8a3f4a80d132d686fd3b7443bc2250023bdb9303190f74c9220481cf99da30b5ec2bdb5a49028f5014e3eaeaa48429a0c78ebd3bb7c7d582c22b7d547cd269f0c4490373a81bf92687e73dac2075b4bda189ce0be225f5f510655e37a6e724a1415bede0a076b92a882cc2a82878ba67aaedf71454eb42b7f8638df8e21d5f708006e5112e2dc0a4afbcfed9f2c7959be812853ca8e313fbc99a0f38f1ee4479c96ccb836632b0808401db159bd2637f7a664013241e4664e994a0a9a3940115a702c60381e66d291e1ade1be2802e1226e311e3201a7c9682b6bc4354caff3d439adb1dfee53ad3fb3dd5e169d64796853bb323129f41213b166a7cac00f728c3e33bd7e59aa2ac0d1341cdb1532b507a0f446e51022a882ac16405442347b70f78c9b6e122f8e70096a4fae4c0405db5b869e0b7b59b09519c4dbf4d4980483906e837da0bee93f668ffaad37d6a4764211a02f95ad2dc2d942c198796741c20a3baf8efb5a53bd9c1a0148318d60a97d0013ab63269097ea295d62c1426d064f0b31c02e74a348ee0509998e701069f5a1e0c1086aed38d2ec87da69fb57a992d88ace3b4a16b0960f5a94936e2e684a9926cf4f911969a2a5d31fed0c7616d30197848253170e51274278873b11f3f5cc1b04b14aa5812524e4d86cbf08306c2aa671288324d7a009b2be533b1d7d0ce6defeeb630b86a9655f1e6424fcb559ed67457c115fba0d0719374802ea68fab299fd3f273be86fa3d2e7456020db2f47c6ec16c21ce6ec65de495e20af1941a5dcd65d910c1cb93f22e1318c173c645c81aed681c9704a8a541ac3d6ff604f46d0260468acbfec1b771b9eb8cd49a2124468dae786571895a569aae18438eaee6343ab2634823119fa2439634645d12e3b4a748b9cc0398b8416a834eb5d9e5cf619bbfaba4894d1c574c738caf530d0862f4cc75eb52bd3921d2d9edb09940edb1e3776423b0046d870ccdcc5d61f72e0440b97a93eeef21fb246a779d339be301a5971400749d6cc9911dfbf9de8ae86fac83c860fdd0e2bfa40af37c99d50e50fd6e5ae86597a201112ed404042b55e132f243dec481a2adc1d5e4b71e1efdea806ea900b2907ce877742d5ecf700ff3640f737863d0dd7207e462ee8d0e17d52047a88ae7446f419560d23968bf64957949e36953155b0ac2511c66be2890b4036329a21e132efb635297a64431899e0c351e50c6682c9b4d79b5d122466d02cd84f206369417d9c194a9349d3c631d72eb7857a9cd542906fc02ad6cdcf9bcf25ace3d826b6623fa5164351e14d3f0de5c8445a2ba3aae26595d0e31c3e307c1d56d4274f61f056145c1b8d6880872b9b10a8bfa4a923cad2edbcf5c50eba48936ed2bcc0be60eb721a74b46704aaae5ad24e2797852195dfacbb30a777d33b63d4dc4f35cfbe5e88fd1944c55a54fd53581446ea061ad29f4671da819ad7488c5dfc700f5f7a1b2af0d6a6e9d9ffc570a6d3209614ab4dc43728f3f0cd7eb4ce36ccd98936bbcbd32627384434bd01e9c0f93b2a5173fba184685e19b9af78afe876aa4e4b4242382b293133771d95a2bd83fa9c62", + "node_privkey": "4242424242424242424242424242424242424242424242424242424242424242", + "next_blinding": "034e09f450a80c3d252b258aba0a61215bf60dda3b0dc78ffb0736ea1259dfd8a0" + }, + { + "alias": "Carol", + "onion": "000288b48876fb0dc0d7375233ccaf2910dc0dc81ba52e5a7906f00d75e0d58dbd4bb7c2714870529410735f0951e72cbe981e2e167c0d8f3de33a36e39e78465aea2acad1e23c78b6fd342d63e37d214c912b4a0be344618f779138edc1b42a5ca3218ca2fea4be427f6cd0d387160db2bf6c2ba8e82941c8cf3626bd6bed7187f633012ef49df38f6b12963cb639e9eed1b9d269dcebcbd0b25287aa536ec85e7320b02e193122199a745ccbaaebd37f5d4b71f52f9b50feeb793eeef56924a046bc5e7003f6253e0284a8d3fe2e42c3564050f1e753cd32cc258ac0ffa6e05eecad5ba1286f78252e60dd884a65405ab673a85ba52adfa65c1086d4bb37ba2e0848adb2b04379775ad798492b14e8997f30ffa9cf5d432bdf5b246fce008fd876399beed827db58195f4f6192f6ff4ec63cb17fdcb497cb7aec26846a71dd8dca02fc3bb14dd7231a4d62a981bec54b71eb20331096dfa214a0ff4489ee96db663826ae8c850e9f06baa52a47b8eb576363f97e742aab2dc616acc6e74588e1d2ac16694febc90abaf5b1c684163c0e615a68d32633f01934adc8c6bf91fa3fd7aad033b7596d60402494e45e2c1632c40f7bfbd88a81a896a1d28ed6338c83e1eeaa467945d59998eb456c95f94bf1892e8f326ec2d5e0196b7073f106febc6ab8ca5bcc23f77ffc819bc1b5debce418ccc7d8391bbf33bceee6110beba170121bd99f54c956e64970bdab31227b03ee0ea3f01fbd9bd74015f6f82d04fab072e8f85f4370d09f41ee3e48eb959767bd989abb4eea42c4daa0437a7f747d7f9b70eb87b9f9b0b6f283b8205912601a432999b8869fd9fe5bad3572edac24da7184f9298f21ff60923db277264d29c846dd2f228f6fc53b6b60364237de64773f803f174ed10229c374f603ccc5fd3a62cb413ffe6f5630dc646bb33f231b2350537ec39e5d3f2fe1a1cb019ed0b18ad14019cad27afcca8ad70387ca110394c0432774f1aa1fa404b2e086c84a55388d3bd102501c78ef925cce89d76fa04c3f20f2d1f0ce507ac8b37b7913e3949ba12bbc5a4f6bac37c2415622d365bc8b83709a28e3d46f3850c89a3ff4d027fef6e3e4ce5c6c85f663c7eaec3c9730106fb82f53249a905533cfabee812aae51965b24b42f7ab471967bc8e73354e69141ee26a1f03684d5fb9c256a34de8257210e0390dd3962db521ae0a3bdab28300610ab2a634b699e5f092da5a061609ef6414bd805c8171f54ad6f285fb64ce0becca0b61188badcf8ef21190dad629e3fb3e89f55ebba829919540ebf5f8ae4283836d3c9133c1ca3365f6b9394916730411650686e0c2ab9c53b6cda9efdd5cfcb53ba9b6962bb6aa49d0a83a87460b60a9c7d2643ee99afe652883795f14014ec5df61b1e30c041c1fa6487f3c82f1ded5f83ffbef5017e197b7fb77be3b36e284a15e57d45bf9316dcaf97eb78ee4642b731ba05c5063bce1333fab4af6da97c80a96ee599b4df823efbedc250c0abba9783da7ddf2414b2a4774ff2880a7dc6791103e18b8631e39743cf9e87aed71700daa5dc72fdae520324741f92ea3d510ff555dea5e45f15cda87272d4559a12d4777680acb06993840e3c748da82c16cae556015fb2acd0335da11a3388575394048ab71199793ab706abc9d68add2075d79a5cc0f779845ee8b98951be61fd293d6c15b9d4653935bf17cf50bd31f8b79e60dba0e7fd6864754fd94262485a4f65e7eb3e1922f51b1a4dd2b4fd2c20d94d1213fbe90bd603dfc7e15176382e3ce0f43f980d44d23bf3c57f54a15f42c171a8f2511e28ac178c6f01396e50397a57ffb09c5e6c315bd3ae7983577c1a0386c6d5d9a2223438e321b0fedfdee58fa452d57dc11a256834bb49ac9deeec88e4bf563c7340f44a240caec941c7e50f09cf", + "node_privkey": "4343434343434343434343434343434343434343434343434343434343434343", + "next_blinding": "031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f" + }, + { + "alias": "Dave", + "onion": "0003f25471c0f2ff549a7fd7859100306bb6c294c209f34c77f782897f184b967c498efc246bdb8e060a6d1cf8dd0d4d732e33311fb96c9e9f1274005fa3d08b41704a1b7224c6300a7caead7baa0a8263eba2e0de6956ee8e4a1958264f47e4cf20d194eb576f5bd249ee4fece563f80fd76dc3eaca8f956188406d83195752b5c90c4b2a5e7ac3a8d5c62b17b551aff48ef6842a7e9326832c9a4a2fd415011150a9e71beb901fd9747bac8add1c694b612730dc86b5b19a0bbbc675947a953316e3303d7b30c182f94def9206671edac9a3ec3e52d28fc28247a1c73ab751bf61c82c3950f617e758f79bd0ba294defb20466eaf1e801462046baad3aec3e5b8868a7b037f23d73a47a7e74c77107334f37388cff863e452820c61d89728fa75c84bc7cdfc06dcdd1911f5f803353926d073efd65251380e174913aae03318ea5b6f0ec83998c55ab99bef62803ea2da9f6d1ea892b90efc4f8ffb685a5201a781da2e6ac5923645638c9709ae32171a00c0cd3d8c7eedfb06b4eedc7d3e566987e2e3805a038f21d78ded5d6c7137a5e8e592f3180ee4d5f4e1289176f67fc38690d0958bc82e240b72b10577f340f1e14b8633f0b6d9729ff4618be2a972400a015a871ba33be70335f652a8d70f2bd32421d6ac2af781d667dad787d6aef4505a15d046579e46eebe757444cffca6d0610f0dd36a7ce57af969bd0c3f7006298ef406a25f689daf58f875d44d2423ebf195b503f11c37c506ea6abe50a463f7bb5e9b964604d832724de768513f6b38bf71715e1feea8a6e86797788d487146891564919da1372016ed8f08c7fcbff66a4a65a3d0fcd8e3daac6eba41f5d65ef2d8075364a9e78b3a273549f6eac4abb72e912e237990367e0d43e89945f8ac3907be5a6c662139485a50cb5ce3f0ba08586c39f6c515368ec3f91b72295f1b7a73a9df322ae9a45d363d6c616be3300083764cbdee31221f25a318f095feacb09f957c96db30fccca47a0215b576c3ed925a0bad05d6400abe318c11f36628c387a4ee38832182cd44b3cd48e5422c1f1e3b57218dfe72c611f5415127720e60f6e2400607e61841b76de1704bcbeb0daf1377ccb2253916de2b6d490bb71ba0a44fea2e94f2423d723934557d5905e01b2b80232a884e258d46dc92ea11e0818d0ece5b914f02049866e151801ab8c9aea155479b354dc91151fb9ba43277458f9760dd859faaa139e3b9ab36a1dbc36a93ef2c90598b20cb30ef3c4f23a2d6178b4d1da668fb328a25d84d30a132d9f2a6a988cbe2e5c2be01cb6db4b4725a50d6cdacf5fb083e7d650a25bec1407fbc047d26076c7596429a29606ad527e97ef0824ad6c1b05831a3e5b71c63a528918a3301cdd4061fc1fcce3da601961f2602a2b002ac8404125c2d52666263858a923e197efcda873c32d86897352e4f2264ad6a1b48acc0fe78ff55cb442cb2bb5fa2880810e1d00aa0247057fb80b7ed36cf9647af41b44ee4a63ee2d6f652526404572520a7d2d9dcde4e62df0c3be89f8471550594cdd16a51a9cacc58729c092c68506162fe65edc2314055d389f724ced189d826a546b5c4d08a43d977b3cf033de5760b71a7cc38ee5851592031aafb467a89b3b6c7ed67b15d44c48d6baedce3e95e08ec7c55038f3eba90ccb900895734f0fb7efe54961ce493369cc56416898a9bed7c2482871c15a7f1eb5ed17c33657fc31333539c2dfb59461af09e7049228113b5c9feea5a6e9959c18c51b19c90995afb9c76f2c0c820964cd7989c993a73925818a656c6a18dcd1a1e3782b2eae06dd5a41250ec2d1c203626ab9920c1673339eff04b1eb0cab85ef5909f571f9b83cdf21697c9f5cfa1c76e7bca955510e2126b3bb989a4ac21cf948f965e48bc363d2997437797b4f770e8b65", + "node_privkey": "4444444444444444444444444444444444444444444444444444444444444444", + "next_blinding": "03e09038ee76e50f444b19abf0a555e8697e035f62937168b80adf0931b31ce52a" + }, + { + "alias": "Eve", + "onion": "0002ef43c4dfe9aee14248c445406ac7980474ce106c504d9025f57963739130adfd06eb26201baee8866af2d1b7a7ab26595349dad002af0590193aaa8f400ab394f5994ec831aeeecb64421c566e3556cbdd7e7e50deb1fc49fd5e007308ab6494415514abff978899623f9b6065ca1e243bb78170118e8b8c8b53b750b59cc1ec017d167adbb3aabab7c2d84fbf94f5d827239f4c2b9d2c3cfe68fe5641f25e386202a4b6edff2a71e700229df7230c8ca31bd5588f04799e9640c9c20a47cba713f3cc5ad3202e14bb520880f2a8409d8e7835cae21b48a651c2d47fe6af785889ab98f1416f6e4ad67a66ae681e9a8828bad3f9b6890221c4a7ec80531d6b63eb30843f613ce644795bc8bcee60e8f7b36f3fd04de762f103c52efaf36a2f3bbbaac482d6271dc4180c10bcc076c04d06ea7fd8fb6a647e0e10523b05da2d89e4139fb55c2315cd01bdcbd57587fef8442d7ff5620630fd2d2e79739d90be811bf2cba60415d6cba2cea14ba1859f3122cd905c4e12e3e2a1ab6fab54b2ec40e434626e2d3c3195c02c82a8bd64d226c2328ac72ca12197d9908eaf54333717448ce6ed73adc0ac05e2ee1d735131d87918beb8995993dc8f63fe10f2c8eba2be7ab8bb44d9f78f59ef3e4c180bd75e4eef2381450c6f0480d543997305f1d07815993b5aca8d88d474966d9abec93bb069a16aa2da75b87f94576e01d08a17d3e0e3d0370f010733a7d7affb12cdf94c259a62607fce71003535c4727305de5ff7bba3840922844b3a45f62c29715fccf440517ef121450f6962396fba9b07036d085582405dcae6ee95964b66bc7c85b8d02d90091500db3cebf6de584f86b7b55335a8c9aa26381b00747f055cc458a2cadfccf9c29702bf941447beaca6583cca09492a57d4b03b2ca00dbaf41dfd6a9b249381626a7debe475735a7e39e77a363eccf14669046f656cc09ad448da8d8b545e6a604f46dc481786d09a94c63cf23f49ba367d2929466364dbce2a8ffce3dadf8f4cef8a56e1fefa1a3304a953fe83018e57d8a95694b02d994fea2630a9a3d5f1e2f6d6142d503ec4152871f7122d7e566a03261f554639e7a759e0e73846f71d5cace37d91336fc9ca9396bf64ca2cf45fa2db779b3b5c63b04f1c0c1fb79fdfcf5a82b0202df934ae1720a7ce1e047cbec3f82737b50168c974f4623cacce87e3f5bd5232caca7956d28ffedcf11ac5998662c5f6b13c6126584ca2e894d3fcbad4d130bbe22e88a135e0020cdd43853e0b3af3800e9544854d211e873cf68ab683578d501d69ec5dc7fce42ac436d58243880c1b88227b0681c6c9dd8a8ad0793202b15ab63b787b748e258da3e68d0e649fc4ac081a71de8adbc891c113d5f722686b6ac4ed9e3cc247bc4a4643416f480627e9de20f7307f434a499f5c6951c2e8b3ff51d455bf65ceb5ee3dee47b968ac2642e13d8a68f903b73627c2e75788fecca5836371a908eea4f1ea44db2315bc185f77e478efeaaa4da2da13fe7aeaa79ed1d04876a8b2b7b333c5de8c4c9a50274c2eb7b9bd2a3630c57173174781fc9785235f830cefa1c82080eaffdef257f18eedc9ddfd25a696a11a3dc56cd836be72f5f4a2cbb6316d5d3b1ad91a7ec7d877f28d2c29a5525b0b24362699281b0e3b48f38caf1085045fe9089f9e6fb29e4b47aa4cecf68c9bf72073469bd9beeea5e88bfe554cb6a81231149ba7fe7784c154fd8b0f9179ecdf1e9fd5c2939ec1ab16df9cbe9359101ebce933d4f65d3f66f87afaecfe9c046b52f4878b6c430329df7bd879fba8864fcbd9b782bf545734699b9b5a66b466dcedc0c9368803b5b0f1232950cef398ad3e057a5db964bd3e5c8a5717b30b41601a4f11ad63afe404cb6f1e8ea5fd7a8e085b65ca5136146febf4d47928dcc9a9e0", + "node_privkey": "4545454545454545454545454545454545454545454545454545454545454545", + "next_blinding": "038fc6859a402b96ce4998c537c823d6ab94d1598fca02c788ba5dd79fbae83589" + } + ] + } +} \ No newline at end of file diff --git a/testdata/onion-test.json b/testdata/onion-test.json new file mode 100644 index 0000000..193256f --- /dev/null +++ b/testdata/onion-test.json @@ -0,0 +1,37 @@ +{ + "comment": "A testcase for a variable length hop_payload. The third payload is 275 bytes long.", + "generate": { + "session_key": "4141414141414141414141414141414141414141414141414141414141414141", + "associated_data": "4242424242424242424242424242424242424242424242424242424242424242", + "hops": [ + { + "pubkey": "02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619", + "payload": "1202023a98040205dc06080000000000000001" + }, + { + "pubkey": "0324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c", + "payload": "52020236b00402057806080000000000000002fd02013c0102030405060708090a0b0c0d0e0f0102030405060708090a0b0c0d0e0f0102030405060708090a0b0c0d0e0f0102030405060708090a0b0c0d0e0f" + }, + { + "pubkey": "027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007", + "payload": "12020230d4040204e206080000000000000003" + }, + { + "pubkey": "032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991", + "payload": "1202022710040203e806080000000000000004" + }, + { + "pubkey": "02edabbd16b41c8371b92ef2f04c1185b4f03b6dcd52ba9b78d9d7c89c8f221145", + "payload": "fd011002022710040203e8082224a33562c54507a9334e79f0dc4f17d407e6d7c61f0e2f3d0d38599502f617042710fd012de02a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a" + } + ] + }, + "onion": "0002eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619f7f3416a5aa36dc7eeb3ec6d421e9615471ab870a33ac07fa5d5a51df0a8823aabe3fea3f90d387529d4f72837f9e687230371ccd8d263072206dbed0234f6505e21e282abd8c0e4f5b9ff8042800bbab065036eadd0149b37f27dde664725a49866e052e809d2b0198ab9610faa656bbf4ec516763a59f8f42c171b179166ba38958d4f51b39b3e98706e2d14a2dafd6a5df808093abfca5aeaaca16eded5db7d21fb0294dd1a163edf0fb445d5c8d7d688d6dd9c541762bf5a5123bf9939d957fe648416e88f1b0928bfa034982b22548e1a4d922690eecf546275afb233acf4323974680779f1a964cfe687456035cc0fba8a5428430b390f0057b6d1fe9a8875bfa89693eeb838ce59f09d207a503ee6f6299c92d6361bc335fcbf9b5cd44747aadce2ce6069cfdc3d671daef9f8ae590cf93d957c9e873e9a1bc62d9640dc8fc39c14902d49a1c80239b6c5b7fd91d05878cbf5ffc7db2569f47c43d6c0d27c438abff276e87364deb8858a37e5a62c446af95d8b786eaf0b5fcf78d98b41496794f8dcaac4eef34b2acfb94c7e8c32a9e9866a8fa0b6f2a06f00a1ccde569f97eec05c803ba7500acc96691d8898d73d8e6a47b8f43c3d5de74458d20eda61474c426359677001fbd75a74d7d5db6cb4feb83122f133206203e4e2d293f838bf8c8b3a29acb321315100b87e80e0edb272ee80fda944e3fb6084ed4d7f7c7d21c69d9da43d31a90b70693f9b0cc3eac74c11ab8ff655905688916cfa4ef0bd04135f2e50b7c689a21d04e8e981e74c6058188b9b1f9dfc3eec6838e9ffbcf22ce738d8a177c19318dffef090cee67e12de1a3e2a39f61247547ba5257489cbc11d7d91ed34617fcc42f7a9da2e3cf31a94a210a1018143173913c38f60e62b24bf0d7518f38b5bab3e6a1f8aeb35e31d6442c8abb5178efc892d2e787d79c6ad9e2fc271792983fa9955ac4d1d84a36c024071bc6e431b625519d556af38185601f70e29035ea6a09c8b676c9d88cf7e05e0f17098b584c4168735940263f940033a220f40be4c85344128b14beb9e75696db37014107801a59b13e89cd9d2258c169d523be6d31552c44c82ff4bb18ec9f099f3bf0e5b1bb2ba9a87d7e26f98d294927b600b5529c47e04d98956677cbcee8fa2b60f49776d8b8c367465b7c626da53700684fb6c918ead0eab8360e4f60edd25b4f43816a75ecf70f909301825b512469f8389d79402311d8aecb7b3ef8599e79485a4388d87744d899f7c47ee644361e17040a7958c8911be6f463ab6a9b2afacd688ec55ef517b38f1339efc54487232798bb25522ff4572ff68567fe830f92f7b8113efce3e98c3fffbaedce4fd8b50e41da97c0c08e423a72689cc68e68f752a5e3a9003e64e35c957ca2e1c48bb6f64b05f56b70b575ad2f278d57850a7ad568c24a4d32a3d74b29f03dc125488bc7c637da582357f40b0a52d16b3b40bb2c2315d03360bc24209e20972c200566bcf3bbe5c5b0aedd83132a8a4d5b4242ba370b6d67d9b67eb01052d132c7866b9cb502e44796d9d356e4e3cb47cc527322cd24976fe7c9257a2864151a38e568ef7a79f10d6ef27cc04ce382347a2488b1f404fdbf407fe1ca1c9d0d5649e34800e25e18951c98cae9f43555eef65fee1ea8f15828807366c3b612cd5753bf9fb8fced08855f742cddd6f765f74254f03186683d646e6f09ac2805586c7cf11998357cafc5df3f285329366f475130c928b2dceba4aa383758e7a9d20705c4bb9db619e2992f608a1ba65db254bb389468741d0502e2588aeb54390ac600c19af5c8e61383fc1bebe0029e4474051e4ef908828db9cca13277ef65db3fd47ccc2179126aaefb627719f421e20", + "decode": [ + "4141414141414141414141414141414141414141414141414141414141414141", + "4242424242424242424242424242424242424242424242424242424242424242", + "4343434343434343434343434343434343434343434343434343434343434343", + "4444444444444444444444444444444444444444444444444444444444444444", + "4545454545454545454545454545454545454545454545454545454545454545" + ] +} diff --git a/testdata/route-blinding-test.json b/testdata/route-blinding-test.json new file mode 100644 index 0000000..bba9031 --- /dev/null +++ b/testdata/route-blinding-test.json @@ -0,0 +1,179 @@ +{ + "comment": "test vector for using blinded routes", + "generate": { + "comment": "This section contains test data for creating a blinded route. This route is the concatenation of two blinded routes: one from Dave to Eve and one from Bob to Carol.", + "hops": [ + { + "comment": "Bob creates a Bob -> Carol route with the following session_key and concatenates it with the Dave -> Eve route.", + "session_key": "0202020202020202020202020202020202020202020202020202020202020202", + "alias": "Bob", + "node_id": "0324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c", + "tlvs": { + "padding": "0000000000000000000000000000000000000000000000000000", + "short_channel_id": "0x0x1729", + "payment_relay": { + "cltv_expiry_delta": 36, + "fee_proportional_millionths": 150, + "fee_base_msat": 10000 + }, + "payment_constraints": { + "max_cltv_expiry": 748005, + "htlc_minimum_msat": 1500 + }, + "allowed_features": { + "features": [] + }, + "unknown_tag_561": "123456" + }, + "encoded_tlvs": "011a0000000000000000000000000000000000000000000000000000020800000000000006c10a0800240000009627100c06000b69e505dc0e00fd023103123456", + "ephemeral_privkey": "0202020202020202020202020202020202020202020202020202020202020202", + "ephemeral_pubkey": "024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d0766", + "shared_secret": "76771bab0cc3d0de6e6f60147fd7c9c7249a5ced3d0612bdfaeec3b15452229d", + "rho": "ba217b23c0978d84c4a19be8a9ff64bc1b40ed0d7ecf59521567a5b3a9a1dd48", + "encrypted_data": "cd4100ff9c09ed28102b210ac73aa12d63e90852cebc496c49f57c49982088b49f2e70b99287fdee0aa58aa39913ab405813b999f66783aa2fe637b3cda91ffc0913c30324e2c6ce327e045183e4bffecb", + "blinded_node_id": "03da173ad2aee2f701f17e59fbd16cb708906d69838a5f088e8123fb36e89a2c25" + }, + { + "comment": "Notice the next_blinding_override tlv in Carol's payload, indicating that Bob concatenated his route with another blinded route starting at Dave.", + "alias": "Carol", + "node_id": "027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007", + "tlvs": { + "short_channel_id": "0x0x1105", + "next_blinding_override": "031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f", + "payment_relay": { + "cltv_expiry_delta": 48, + "fee_proportional_millionths": 100, + "fee_base_msat": 500 + }, + "payment_constraints": { + "max_cltv_expiry": 747969, + "htlc_minimum_msat": 1500 + }, + "allowed_features": { + "features": [] + } + }, + "encoded_tlvs": "020800000000000004510821031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f0a0800300000006401f40c06000b69c105dc0e00", + "ephemeral_privkey": "0a2aa791ac81265c139237b2b84564f6000b1d4d0e68d4b9cc97c5536c9b61c1", + "ephemeral_pubkey": "034e09f450a80c3d252b258aba0a61215bf60dda3b0dc78ffb0736ea1259dfd8a0", + "shared_secret": "dc91516ec6b530a3d641c01f29b36ed4dc29a74e063258278c0eeed50313d9b8", + "rho": "d1e62bae1a8e169da08e6204997b60b1a7971e0f246814c648125c35660f5416", + "encrypted_data": "cc0f16524fd7f8bb0b1d8d40ad71709ef140174c76faa574cac401bb8992fef76c4d004aa485dd599ed1cf2715f57ff62da5aaec5d7b10d59b04d8a9d77e472b9b3ecc2179334e411be22fa4c02b467c7e", + "blinded_node_id": "02e466727716f044290abf91a14a6d90e87487da160c2a3cbd0d465d7a78eb83a7" + }, + { + "comment": "Eve creates a Dave -> Eve blinded route using the following session_key.", + "session_key": "0101010101010101010101010101010101010101010101010101010101010101", + "alias": "Dave", + "node_id": "032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991", + "tlvs": { + "padding": "0000000000000000000000000000000000000000000000000000000000000000000000", + "short_channel_id": "0x0x561", + "payment_relay": { + "cltv_expiry_delta": 144, + "fee_proportional_millionths": 250 + }, + "payment_constraints": { + "max_cltv_expiry": 747921, + "htlc_minimum_msat": 1500 + }, + "allowed_features": { + "features": [] + } + }, + "encoded_tlvs": "01230000000000000000000000000000000000000000000000000000000000000000000000020800000000000002310a060090000000fa0c06000b699105dc0e00", + "ephemeral_privkey": "0101010101010101010101010101010101010101010101010101010101010101", + "ephemeral_pubkey": "031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f", + "shared_secret": "dc46f3d1d99a536300f17bc0512376cc24b9502c5d30144674bfaa4b923d9057", + "rho": "393aa55d35c9e207a8f28180b81628a31dff558c84959cdc73130f8c321d6a06", + "encrypted_data": "0fa0a72cff3b64a3d6e1e4903cf8c8b0a17144aeb249dcb86561adee1f679ee8db3e561d9c43815fd4bcebf6f58c546da0cd8a9bf5cebd0d554802f6c0255e28e4a27343f761fe518cd897463187991105", + "blinded_node_id": "036861b366f284f0a11738ffbf7eda46241a8977592878fe3175ae1d1e4754eccf" + }, + { + "comment": "Eve is the final recipient, so she included a path_id in her own payload to verify that the route is used when she expects it.", + "alias": "Eve", + "node_id": "02edabbd16b41c8371b92ef2f04c1185b4f03b6dcd52ba9b78d9d7c89c8f221145", + "tlvs": { + "padding": "0000000000000000000000000000000000000000000000000000", + "path_id": "deadbeef", + "payment_constraints": { + "max_cltv_expiry": 747777, + "htlc_minimum_msat": 1500 + }, + "allowed_features": { + "features": [113] + }, + "unknown_tag_65535": "06c1" + }, + "encoded_tlvs": "011a00000000000000000000000000000000000000000000000000000604deadbeef0c06000b690105dc0e0f020000000000000000000000000000fdffff0206c1", + "ephemeral_privkey": "62e8bcd6b5f7affe29bec4f0515aab2eebd1ce848f4746a9638aa14e3024fb1b", + "ephemeral_pubkey": "03e09038ee76e50f444b19abf0a555e8697e035f62937168b80adf0931b31ce52a", + "shared_secret": "352a706b194c2b6d0a04ba1f617383fb816dc5f8f9ac0b60dd19c9ae3b517289", + "rho": "719d0307340b1c68b79865111f0de6e97b093a30bc603cebd1beb9eef116f2d8", + "encrypted_data": "da1a7e5f7881219884beae6ae68971de73bab4c3055d9865b1afb60724a2e4d3f0489ad884f7f3f77149209f0df51efd6b276294a02e3949c7254fbc8b5cab58212d9a78983e1cf86fe218b30c4ca8f6d8", + "blinded_node_id": "021982a48086cb8984427d3727fe35a03d396b234f0701f5249daa12e8105c8dae" + } + ] + }, + "route": { + "comment": "This section contains the resulting blinded route, which can then be used inside onion messages or payments.", + "introduction_node_id": "0324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c", + "blinding": "024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d0766", + "hops": [ + { + "blinded_node_id": "03da173ad2aee2f701f17e59fbd16cb708906d69838a5f088e8123fb36e89a2c25", + "encrypted_data": "cd4100ff9c09ed28102b210ac73aa12d63e90852cebc496c49f57c49982088b49f2e70b99287fdee0aa58aa39913ab405813b999f66783aa2fe637b3cda91ffc0913c30324e2c6ce327e045183e4bffecb" + }, + { + "blinded_node_id": "02e466727716f044290abf91a14a6d90e87487da160c2a3cbd0d465d7a78eb83a7", + "encrypted_data": "cc0f16524fd7f8bb0b1d8d40ad71709ef140174c76faa574cac401bb8992fef76c4d004aa485dd599ed1cf2715f57ff62da5aaec5d7b10d59b04d8a9d77e472b9b3ecc2179334e411be22fa4c02b467c7e" + }, + { + "blinded_node_id": "036861b366f284f0a11738ffbf7eda46241a8977592878fe3175ae1d1e4754eccf", + "encrypted_data": "0fa0a72cff3b64a3d6e1e4903cf8c8b0a17144aeb249dcb86561adee1f679ee8db3e561d9c43815fd4bcebf6f58c546da0cd8a9bf5cebd0d554802f6c0255e28e4a27343f761fe518cd897463187991105" + }, + { + "blinded_node_id": "021982a48086cb8984427d3727fe35a03d396b234f0701f5249daa12e8105c8dae", + "encrypted_data": "da1a7e5f7881219884beae6ae68971de73bab4c3055d9865b1afb60724a2e4d3f0489ad884f7f3f77149209f0df51efd6b276294a02e3949c7254fbc8b5cab58212d9a78983e1cf86fe218b30c4ca8f6d8" + } + ] + }, + "unblind": { + "comment": "This section contains test data for unblinding the route at each intermediate hop.", + "hops": [ + { + "alias": "Bob", + "node_privkey": "4242424242424242424242424242424242424242424242424242424242424242", + "ephemeral_pubkey": "024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d0766", + "blinded_privkey": "d12fec0332c3e9d224789a17ebd93595f37d37bd8ef8bd3d2e6ce50acb9e554f", + "decrypted_data": "011a0000000000000000000000000000000000000000000000000000020800000000000006c10a0800240000009627100c06000b69e505dc0e00fd023103123456", + "next_ephemeral_pubkey": "034e09f450a80c3d252b258aba0a61215bf60dda3b0dc78ffb0736ea1259dfd8a0" + }, + { + "alias": "Carol", + "node_privkey": "4343434343434343434343434343434343434343434343434343434343434343", + "ephemeral_pubkey": "034e09f450a80c3d252b258aba0a61215bf60dda3b0dc78ffb0736ea1259dfd8a0", + "blinded_privkey": "bfa697fbbc8bbc43ca076e6dd60d306038a32af216b9dc6fc4e59e5ae28823c1", + "decrypted_data": "020800000000000004510821031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f0a0800300000006401f40c06000b69c105dc0e00", + "next_ephemeral_pubkey": "03af5ccc91851cb294e3a364ce63347709a08cdffa58c672e9a5c587ddd1bbca60", + "next_ephemeral_pubkey_override": "031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f" + }, + { + "alias": "Dave", + "node_privkey": "4444444444444444444444444444444444444444444444444444444444444444", + "ephemeral_pubkey": "031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f", + "blinded_privkey": "cebc115c7fce4c295dc396dea6c79115b289b8ceeceea2ed61cf31428d88fc4e", + "decrypted_data": "01230000000000000000000000000000000000000000000000000000000000000000000000020800000000000002310a060090000000fa0c06000b699105dc0e00", + "next_ephemeral_pubkey": "03e09038ee76e50f444b19abf0a555e8697e035f62937168b80adf0931b31ce52a" + }, + { + "alias": "Eve", + "node_privkey": "4545454545454545454545454545454545454545454545454545454545454545", + "ephemeral_pubkey": "03e09038ee76e50f444b19abf0a555e8697e035f62937168b80adf0931b31ce52a", + "blinded_privkey": "ff4e07da8d92838bedd019ce532eb990ed73b574e54a67862a1df81b40c0d2af", + "decrypted_data": "011a00000000000000000000000000000000000000000000000000000604deadbeef0c06000b690105dc0e0f020000000000000000000000000000fdffff0206c1", + "next_ephemeral_pubkey": "038fc6859a402b96ce4998c537c823d6ab94d1598fca02c788ba5dd79fbae83589" + } + ] + } +} \ No newline at end of file