Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NodeId #601

Merged
merged 3 commits into from
Feb 15, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that you'll need to be careful when decrypting an onion message and decoding its content to catch that exception.

}
}

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 @@ -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"))))
t-bast marked this conversation as resolved.
Show resolved Hide resolved
)
),
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
Loading