Skip to content

Commit

Permalink
Add EncodedNodeId (#601)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
thomash-acinq authored Feb 15, 2024
1 parent 77003f9 commit 34e6834
Show file tree
Hide file tree
Showing 8 changed files with 98 additions and 29 deletions.
22 changes: 22 additions & 0 deletions src/commonMain/kotlin/fr/acinq/lightning/EncodedNodeId.kt
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,21 @@ 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.
* @param encryptedPayload encrypted payload that can be decrypted with the introduction node's private key and the
* blinding ephemeral key.
*/
data class IntroductionNode(
val publicKey: PublicKey,
val nodeId: EncodedNodeId,
val blindedPublicKey: PublicKey,
val blindingEphemeralKey: PublicKey,
val encryptedPayload: ByteVector
Expand All @@ -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<BlindedNode>
) {
Expand Down Expand Up @@ -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)
}

/**
Expand Down
29 changes: 25 additions & 4 deletions src/commonMain/kotlin/fr/acinq/lightning/wire/LightningCodecs.kt
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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)
}
}

}
20 changes: 10 additions & 10 deletions src/commonMain/kotlin/fr/acinq/lightning/wire/MessageOnion.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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)
Expand All @@ -29,15 +30,14 @@ sealed class OnionMessagePayloadTlv : Tlv {
companion object : TlvValueReader<ReplyPath> {
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))
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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<OutgoingNodeId> {
const val tag: Long = 4
override fun read(input: Input): OutgoingNodeId =
OutgoingNodeId(PublicKey(LightningCodecs.bytes(input, 33)))
OutgoingNodeId(LightningCodecs.encodedNodeId(input))
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"))
Expand Down Expand Up @@ -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"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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())))
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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(
Expand All @@ -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(
Expand All @@ -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")))
)
),
Expand Down

0 comments on commit 34e6834

Please sign in to comment.