From 34e683435c94432366be58af0aff70f32e3d27d8 Mon Sep 17 00:00:00 2001 From: Thomas HUET <81159533+thomash-acinq@users.noreply.github.com> Date: Thu, 15 Feb 2024 10:15:35 +0100 Subject: [PATCH] Add EncodedNodeId (#601) Adds the EncodedNodeId interface that can be either a public key or a pair channel id, direction that's more compact. This is used as introduction node for blinded routes. We also use EncodedNodeId for the next node to relay a message to. Which is not in the spec yet but is necessary for us since we have no way to resolve the compact node id to a public key ourselves. It requires a compatible peer that accepts this format. --- .../fr/acinq/lightning/EncodedNodeId.kt | 22 ++++++++++++++ .../acinq/lightning/crypto/RouteBlinding.kt | 9 +++--- .../acinq/lightning/wire/LightningCodecs.kt | 29 ++++++++++++++++--- .../fr/acinq/lightning/wire/MessageOnion.kt | 20 ++++++------- .../fr/acinq/lightning/wire/RouteBlinding.kt | 7 +++-- .../crypto/sphinx/SphinxTestsCommon.kt | 7 +++-- .../wire/LightningCodecsTestsCommon.kt | 22 ++++++++++++++ .../wire/RouteBlindingTestsCommon.kt | 11 +++---- 8 files changed, 98 insertions(+), 29 deletions(-) create mode 100644 src/commonMain/kotlin/fr/acinq/lightning/EncodedNodeId.kt diff --git a/src/commonMain/kotlin/fr/acinq/lightning/EncodedNodeId.kt b/src/commonMain/kotlin/fr/acinq/lightning/EncodedNodeId.kt new file mode 100644 index 000000000..35c43c65e --- /dev/null +++ b/src/commonMain/kotlin/fr/acinq/lightning/EncodedNodeId.kt @@ -0,0 +1,22 @@ +package fr.acinq.lightning + +import fr.acinq.bitcoin.PublicKey +import fr.acinq.bitcoin.io.Input +import fr.acinq.bitcoin.io.Output +import fr.acinq.lightning.wire.LightningCodecs + +sealed class EncodedNodeId { + /** Nodes are usually identified by their public key. */ + data class Plain(val publicKey: PublicKey) : EncodedNodeId() { + override fun toString(): String = publicKey.toString() + } + + /** For compactness, nodes may be identified by the shortChannelId of one of their public channels. */ + data class ShortChannelIdDir(val isNode1: Boolean, val scid: ShortChannelId) : EncodedNodeId() { + override fun toString(): String = if (isNode1) "<-$scid" else "$scid->" + } + + companion object { + operator fun invoke(publicKey: PublicKey): EncodedNodeId = Plain(publicKey) + } +} diff --git a/src/commonMain/kotlin/fr/acinq/lightning/crypto/RouteBlinding.kt b/src/commonMain/kotlin/fr/acinq/lightning/crypto/RouteBlinding.kt index 0889859c1..3e223e210 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/crypto/RouteBlinding.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/crypto/RouteBlinding.kt @@ -4,12 +4,13 @@ import fr.acinq.bitcoin.ByteVector import fr.acinq.bitcoin.Crypto import fr.acinq.bitcoin.PrivateKey import fr.acinq.bitcoin.PublicKey +import fr.acinq.lightning.EncodedNodeId import fr.acinq.lightning.crypto.sphinx.Sphinx object RouteBlinding { /** - * @param publicKey introduction node's public key (which cannot be blinded since the sender need to find a route to it). + * @param nodeId introduction node's id (which cannot be blinded since the sender need to find a route to it). * @param blindedPublicKey blinded public key, which hides the real public key. * @param blindingEphemeralKey blinding tweak that can be used by the receiving node to derive the private key that * matches the blinded public key. @@ -17,7 +18,7 @@ object RouteBlinding { * blinding ephemeral key. */ data class IntroductionNode( - val publicKey: PublicKey, + val nodeId: EncodedNodeId, val blindedPublicKey: PublicKey, val blindingEphemeralKey: PublicKey, val encryptedPayload: ByteVector @@ -37,7 +38,7 @@ object RouteBlinding { * @param blindedNodes blinded nodes (including the introduction node). */ data class BlindedRoute( - val introductionNodeId: PublicKey, + val introductionNodeId: EncodedNodeId, val blindingKey: PublicKey, val blindedNodes: List ) { @@ -78,7 +79,7 @@ object RouteBlinding { e *= PrivateKey(Crypto.sha256(blindingKey.value.toByteArray() + sharedSecret.toByteArray())) Pair(BlindedNode(blindedPublicKey, ByteVector(encryptedPayload + mac)), blindingKey) }.unzip() - return BlindedRoute(publicKeys.first(), blindingKeys.first(), blindedHops) + return BlindedRoute(EncodedNodeId(publicKeys.first()), blindingKeys.first(), blindedHops) } /** diff --git a/src/commonMain/kotlin/fr/acinq/lightning/wire/LightningCodecs.kt b/src/commonMain/kotlin/fr/acinq/lightning/wire/LightningCodecs.kt index 4cef0538c..fab90268e 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/wire/LightningCodecs.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/wire/LightningCodecs.kt @@ -1,13 +1,12 @@ package fr.acinq.lightning.wire -import fr.acinq.bitcoin.ByteVector -import fr.acinq.bitcoin.ByteVector32 -import fr.acinq.bitcoin.TxHash -import fr.acinq.bitcoin.TxId +import fr.acinq.bitcoin.* import fr.acinq.bitcoin.crypto.Pack import fr.acinq.bitcoin.io.ByteArrayOutput import fr.acinq.bitcoin.io.Input import fr.acinq.bitcoin.io.Output +import fr.acinq.lightning.EncodedNodeId +import fr.acinq.lightning.ShortChannelId import fr.acinq.lightning.utils.leftPaddedCopyOf import kotlin.jvm.JvmStatic @@ -225,4 +224,26 @@ object LightningCodecs { return bytes(input, length) } + fun encodedNodeId(input: Input): EncodedNodeId { + val firstByte = byte(input) + if (firstByte == 0 || firstByte == 1) { + val isNode1 = firstByte == 0 + val scid = ShortChannelId(int64(input)) + return EncodedNodeId.ShortChannelIdDir(isNode1, scid) + } else if (firstByte == 2 || firstByte == 3) { + val publicKey = PublicKey(ByteArray(1) { firstByte.toByte() } + bytes(input, 32)) + return EncodedNodeId.Plain(publicKey) + } else { + throw IllegalArgumentException("unexpected first byte: $firstByte") + } + } + + fun writeEncodedNodeId(input: EncodedNodeId, out: Output): Unit = when (input) { + is EncodedNodeId.Plain -> writeBytes(input.publicKey.value, out) + is EncodedNodeId.ShortChannelIdDir -> { + writeByte(if (input.isNode1) 0 else 1, out) + writeInt64(input.scid.toLong(), out) + } + } + } \ No newline at end of file diff --git a/src/commonMain/kotlin/fr/acinq/lightning/wire/MessageOnion.kt b/src/commonMain/kotlin/fr/acinq/lightning/wire/MessageOnion.kt index 7adac5354..ad778794d 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/wire/MessageOnion.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/wire/MessageOnion.kt @@ -6,9 +6,9 @@ import fr.acinq.bitcoin.io.ByteArrayInput import fr.acinq.bitcoin.io.ByteArrayOutput import fr.acinq.bitcoin.io.Input import fr.acinq.bitcoin.io.Output +import fr.acinq.lightning.EncodedNodeId import fr.acinq.lightning.crypto.RouteBlinding - sealed class OnionMessagePayloadTlv : Tlv { /** * Onion messages may provide a reply path, allowing the recipient to send a message back to the original sender. @@ -17,8 +17,9 @@ sealed class OnionMessagePayloadTlv : Tlv { data class ReplyPath(val blindedRoute: RouteBlinding.BlindedRoute) : OnionMessagePayloadTlv() { override val tag: Long get() = ReplyPath.tag override fun write(out: Output) { - LightningCodecs.writeBytes(blindedRoute.introductionNodeId.value, out) + LightningCodecs.writeEncodedNodeId(blindedRoute.introductionNodeId, out) LightningCodecs.writeBytes(blindedRoute.blindingKey.value, out) + LightningCodecs.writeByte(blindedRoute.blindedNodes.size, out) for (hop in blindedRoute.blindedNodes) { LightningCodecs.writeBytes(hop.blindedPublicKey.value, out) LightningCodecs.writeU16(hop.encryptedPayload.size(), out) @@ -29,15 +30,14 @@ sealed class OnionMessagePayloadTlv : Tlv { companion object : TlvValueReader { const val tag: Long = 2 override fun read(input: Input): ReplyPath { - val firstNodeId = PublicKey(LightningCodecs.bytes(input, 33)) + val firstNodeId = LightningCodecs.encodedNodeId(input) val blinding = PublicKey(LightningCodecs.bytes(input, 33)) - val path = sequence { - while (input.availableBytes > 0) { - val blindedPublicKey = PublicKey(LightningCodecs.bytes(input, 33)) - val encryptedPayload = ByteVector(LightningCodecs.bytes(input, LightningCodecs.u16(input))) - yield(RouteBlinding.BlindedNode(blindedPublicKey, encryptedPayload)) - } - }.toList() + val numHops = LightningCodecs.byte(input) + val path = (0 until numHops).map { + val blindedPublicKey = PublicKey(LightningCodecs.bytes(input, 33)) + val encryptedPayload = ByteVector(LightningCodecs.bytes(input, LightningCodecs.u16(input))) + RouteBlinding.BlindedNode(blindedPublicKey, encryptedPayload) + } return ReplyPath(RouteBlinding.BlindedRoute(firstNodeId, blinding, path)) } } diff --git a/src/commonMain/kotlin/fr/acinq/lightning/wire/RouteBlinding.kt b/src/commonMain/kotlin/fr/acinq/lightning/wire/RouteBlinding.kt index 0f93a0fca..043a281c0 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/wire/RouteBlinding.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/wire/RouteBlinding.kt @@ -6,6 +6,7 @@ import fr.acinq.bitcoin.io.ByteArrayInput import fr.acinq.bitcoin.io.ByteArrayOutput import fr.acinq.bitcoin.io.Input import fr.acinq.bitcoin.io.Output +import fr.acinq.lightning.EncodedNodeId sealed class RouteBlindingEncryptedDataTlv : Tlv { @@ -22,14 +23,14 @@ sealed class RouteBlindingEncryptedDataTlv : Tlv { } /** Id of the next node. */ - data class OutgoingNodeId(val nodeId: PublicKey) : RouteBlindingEncryptedDataTlv() { + data class OutgoingNodeId(val nodeId: EncodedNodeId) : RouteBlindingEncryptedDataTlv() { override val tag: Long get() = OutgoingNodeId.tag - override fun write(out: Output) = LightningCodecs.writeBytes(nodeId.value, out) + override fun write(out: Output) = LightningCodecs.writeEncodedNodeId(nodeId, out) companion object : TlvValueReader { const val tag: Long = 4 override fun read(input: Input): OutgoingNodeId = - OutgoingNodeId(PublicKey(LightningCodecs.bytes(input, 33))) + OutgoingNodeId(LightningCodecs.encodedNodeId(input)) } } diff --git a/src/commonTest/kotlin/fr/acinq/lightning/crypto/sphinx/SphinxTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/crypto/sphinx/SphinxTestsCommon.kt index 61e0b60c8..d0e18dd2b 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/crypto/sphinx/SphinxTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/crypto/sphinx/SphinxTestsCommon.kt @@ -4,6 +4,7 @@ import fr.acinq.bitcoin.ByteVector import fr.acinq.bitcoin.ByteVector32 import fr.acinq.bitcoin.PrivateKey import fr.acinq.bitcoin.PublicKey +import fr.acinq.lightning.EncodedNodeId import fr.acinq.lightning.crypto.RouteBlinding import fr.acinq.lightning.crypto.sphinx.Sphinx.computeEphemeralPublicKeysAndSharedSecrets import fr.acinq.lightning.crypto.sphinx.Sphinx.decodePayloadLength @@ -562,8 +563,8 @@ class SphinxTestsCommon : LightningTestSuite() { fun `create blinded route -- reference test vector`() { val sessionKey = PrivateKey(ByteVector32("0101010101010101010101010101010101010101010101010101010101010101")) val blindedRoute = RouteBlinding.create(sessionKey, publicKeys, routeBlindingPayloads) - assertEquals(blindedRoute.introductionNode.publicKey, publicKeys[0]) - assertEquals(blindedRoute.introductionNodeId, publicKeys[0]) + assertEquals(blindedRoute.introductionNode.nodeId, EncodedNodeId(publicKeys[0])) + assertEquals(blindedRoute.introductionNodeId, EncodedNodeId(publicKeys[0])) assertEquals(blindedRoute.introductionNode.blindedPublicKey, PublicKey.fromHex("02ec68ed555f5d18b12fe0e2208563c3566032967cf11dc29b20c345449f9a50a2")) assertEquals(blindedRoute.introductionNode.blindingEphemeralKey, PublicKey.fromHex("031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f")) assertEquals(blindedRoute.introductionNode.encryptedPayload, ByteVector("af4fbf67bd52520bdfab6a88cd4e7f22ffad08d8b153b17ff303f93fdb4712")) @@ -641,7 +642,7 @@ class SphinxTestsCommon : LightningTestSuite() { ) Pair(RouteBlinding.create(sessionKey, publicKeys.take(2), payloads), payloads) } - val blindedRoute = RouteBlinding.BlindedRoute(publicKeys[0], blindedRouteStart.blindingKey, blindedRouteStart.blindedNodes + blindedRouteEnd.blindedNodes) + val blindedRoute = RouteBlinding.BlindedRoute(EncodedNodeId(publicKeys[0]), blindedRouteStart.blindingKey, blindedRouteStart.blindedNodes + blindedRouteEnd.blindedNodes) assertEquals(blindedRoute.blindingKey, PublicKey.fromHex("024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d0766")) assertEquals(blindedRoute.blindedNodeIds, listOf( PublicKey.fromHex("0303176d13958a8a59d59517a6223e12cf291ba5f65c8011efcdca0a52c3850abc"), diff --git a/src/commonTest/kotlin/fr/acinq/lightning/wire/LightningCodecsTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/wire/LightningCodecsTestsCommon.kt index 20071b675..b36c02e37 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/wire/LightningCodecsTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/wire/LightningCodecsTestsCommon.kt @@ -818,4 +818,26 @@ class LightningCodecsTestsCommon : LightningTestSuite() { } } + + @Test + fun `encoded node id`() { + val testCases = mapOf( + ByteVector.fromHex("00 0d950b0001c80000") to + EncodedNodeId.ShortChannelIdDir(isNode1 = true, ShortChannelId(890123, 456, 0)), + ByteVector.fromHex("01 0c0a14000d800005") to + EncodedNodeId.ShortChannelIdDir(isNode1 = false, ShortChannelId(789012, 3456, 5)), + ByteVector.fromHex("022d3b15cea00ee4a8e710b082bef18f0f3409cc4e7aff41c26eb0a4d3ab20dd73") to + EncodedNodeId.Plain(PublicKey.fromHex("022d3b15cea00ee4a8e710b082bef18f0f3409cc4e7aff41c26eb0a4d3ab20dd73")), + ByteVector.fromHex("03ba3c458e3299eb19d2e07ae86453f4290bcdf8689707f0862f35194397c45922") to + EncodedNodeId.Plain(PublicKey.fromHex("03ba3c458e3299eb19d2e07ae86453f4290bcdf8689707f0862f35194397c45922")), + ) + + for (testCase in testCases) { + val (encoded, decoded) = testCase + val out = ByteArrayOutput() + LightningCodecs.writeEncodedNodeId(decoded, out) + assertEquals(encoded, out.toByteArray().toByteVector()) + assertEquals(decoded, LightningCodecs.encodedNodeId(ByteArrayInput(encoded.toByteArray()))) + } + } } \ No newline at end of file diff --git a/src/commonTest/kotlin/fr/acinq/lightning/wire/RouteBlindingTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/wire/RouteBlindingTestsCommon.kt index 5d013778a..1e5e32e1a 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/wire/RouteBlindingTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/wire/RouteBlindingTestsCommon.kt @@ -2,6 +2,7 @@ package fr.acinq.lightning.wire import fr.acinq.bitcoin.ByteVector import fr.acinq.bitcoin.PublicKey +import fr.acinq.lightning.EncodedNodeId import fr.acinq.lightning.tests.utils.LightningTestSuite import fr.acinq.lightning.wire.RouteBlindingEncryptedDataTlv.* import kotlin.test.Test @@ -14,7 +15,7 @@ class RouteBlindingTestsCommon : LightningTestSuite() { ByteVector("01080000000000000000 042102edabbd16b41c8371b92ef2f04c1185b4f03b6dcd52ba9b78d9d7c89c8f221145") to RouteBlindingEncryptedData( TlvStream( Padding(ByteVector("0000000000000000")), - OutgoingNodeId(PublicKey(ByteVector("02edabbd16b41c8371b92ef2f04c1185b4f03b6dcd52ba9b78d9d7c89c8f221145"))) + OutgoingNodeId(EncodedNodeId(PublicKey(ByteVector("02edabbd16b41c8371b92ef2f04c1185b4f03b6dcd52ba9b78d9d7c89c8f221145")))) ) ), ByteVector("0109000000000000000000 06204242424242424242424242424242424242424242424242424242424242424242") to RouteBlindingEncryptedData( @@ -24,10 +25,10 @@ class RouteBlindingTestsCommon : LightningTestSuite() { ) ), ByteVector("0421032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991") to RouteBlindingEncryptedData( - TlvStream(OutgoingNodeId(PublicKey(ByteVector("032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991")))) + TlvStream(OutgoingNodeId(EncodedNodeId(PublicKey(ByteVector("032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991"))))) ), ByteVector("042102edabbd16b41c8371b92ef2f04c1185b4f03b6dcd52ba9b78d9d7c89c8f221145") to RouteBlindingEncryptedData( - TlvStream(OutgoingNodeId(PublicKey(ByteVector("02edabbd16b41c8371b92ef2f04c1185b4f03b6dcd52ba9b78d9d7c89c8f221145")))) + TlvStream(OutgoingNodeId(EncodedNodeId(PublicKey(ByteVector("02edabbd16b41c8371b92ef2f04c1185b4f03b6dcd52ba9b78d9d7c89c8f221145"))))) ), ByteVector("010f000000000000000000000000000000 061000112233445566778899aabbccddeeff") to RouteBlindingEncryptedData( TlvStream( @@ -38,12 +39,12 @@ class RouteBlindingTestsCommon : LightningTestSuite() { ByteVector("0121000000000000000000000000000000000000000000000000000000000000000000 04210324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c") to RouteBlindingEncryptedData( TlvStream( Padding(ByteVector("000000000000000000000000000000000000000000000000000000000000000000")), - OutgoingNodeId(PublicKey(ByteVector("0324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c"))) + OutgoingNodeId(EncodedNodeId(PublicKey(ByteVector("0324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c")))) ) ), ByteVector("0421027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007 0821031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f") to RouteBlindingEncryptedData( TlvStream( - OutgoingNodeId(PublicKey(ByteVector("027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007"))), + OutgoingNodeId(EncodedNodeId(PublicKey(ByteVector("027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007")))), NextBlinding(PublicKey(ByteVector("031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f"))) ) ),